mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-12 22:09:48 +08:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f3600d8e5 | |||
| 045fe5e57e | |||
| aa1a967a9f | |||
| d643bad53c | |||
| 0d977787b5 | |||
| 516803feb4 | |||
| 4af0506fa3 | |||
| 80a1c2970a | |||
| cdfa0dbff9 | |||
| 505a7e10ce | |||
| 6d063734ae | |||
| 31d728b505 | |||
| f6d5df0594 | |||
| 4bd5fe573e |
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
||||
"version": "1.111.1"
|
||||
"version": "1.115.0"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: release-skills
|
||||
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", "推送".
|
||||
description: Universal release workflow. Auto-detects version files and changelogs. Supports Node.js, Python, Rust, Claude Plugin, GitHub Releases, annotated tags, historical release backfill, and generic projects. Use when user says "release", "发布", "new version", "bump version", "push", "推送", "release notes", "GitHub Release", or "回填 Release".
|
||||
---
|
||||
|
||||
# Release Skills
|
||||
@@ -39,6 +39,7 @@ Just run `/release-skills` - auto-detects your project configuration.
|
||||
| `--major` | Force major version bump |
|
||||
| `--minor` | Force minor version bump |
|
||||
| `--patch` | Force patch version bump |
|
||||
| `--backfill-releases` | Create missing GitHub Releases for existing tags from changelog sections |
|
||||
|
||||
## Workflow
|
||||
|
||||
@@ -57,7 +58,11 @@ Just run `/release-skills` - auto-detects your project configuration.
|
||||
- `HISTORY*.md`
|
||||
- `CHANGES*.md`
|
||||
4. Identify language of each changelog by filename suffix
|
||||
5. Display detected configuration
|
||||
5. Detect GitHub release support:
|
||||
- Check whether `origin` points to GitHub
|
||||
- Check whether `gh` is installed and authenticated
|
||||
- Check existing releases with `gh release list --limit 5` when available
|
||||
6. Display detected configuration
|
||||
|
||||
**Project Hook Contract**:
|
||||
|
||||
@@ -310,6 +315,14 @@ git commit -m "docs(project): update architecture documentation"
|
||||
- Read version file (JSON/TOML/text)
|
||||
- Update version number
|
||||
- Write back (preserve formatting)
|
||||
3. **Create release notes file**:
|
||||
- Prefer the new version section from `CHANGELOG.md`
|
||||
- If no English/default changelog exists, use the first detected changelog
|
||||
- Extract only the exact `## {VERSION} - {YYYY-MM-DD}` section through the next `##`
|
||||
- Match both plain version and tag-prefixed headings when needed, e.g. `1.2.3` and `v1.2.3`
|
||||
- Keep breaking changes near the top; if needed, add a short highlight before other sections
|
||||
- Write notes to a UTF-8 temp file and reuse it for annotated tag messages, GitHub Releases, and `publish_artifact`
|
||||
- In normal mode, stop rather than creating an empty tag or GitHub Release when notes cannot be found
|
||||
|
||||
**Version Paths by File Type**:
|
||||
|
||||
@@ -325,7 +338,7 @@ git commit -m "docs(project): update architecture documentation"
|
||||
|
||||
Before creating the release commit, ask user to confirm:
|
||||
|
||||
**Use AskUserQuestion with two questions**:
|
||||
**Use AskUserQuestion with three questions**:
|
||||
|
||||
1. **Version bump** (single select):
|
||||
- Show recommended version based on Step 3 analysis
|
||||
@@ -335,6 +348,11 @@ Before creating the release commit, ask user to confirm:
|
||||
2. **Push to remote** (single select):
|
||||
- Options: "Yes, push after commit", "No, keep local only"
|
||||
|
||||
3. **Publish GitHub Release** (single select):
|
||||
- Offer this only when GitHub release support is available
|
||||
- Default to "Yes, publish after tag push" when the user also chose push
|
||||
- If the user keeps the release local, do not create or edit a GitHub Release
|
||||
|
||||
**Example Output Before Confirmation**:
|
||||
```
|
||||
Commits created:
|
||||
@@ -349,10 +367,11 @@ Changelog preview (en):
|
||||
### Fixes
|
||||
- Improve panel layout for long dialogues in comic
|
||||
|
||||
Ready to create release commit and tag.
|
||||
Release notes source: CHANGELOG.md#1.3.0
|
||||
Ready to create release commit, annotated tag, and GitHub Release.
|
||||
```
|
||||
|
||||
### Step 9: Create Release Commit and Tag
|
||||
### Step 9: Create Release Commit and Annotated Tag
|
||||
|
||||
After user confirmation:
|
||||
|
||||
@@ -367,10 +386,11 @@ After user confirmation:
|
||||
git commit -m "chore: release v{VERSION}"
|
||||
```
|
||||
|
||||
3. **Create tag**:
|
||||
3. **Create annotated tag**:
|
||||
```bash
|
||||
git tag v{VERSION}
|
||||
git tag -a v{VERSION} -F <release-notes-file>
|
||||
```
|
||||
If `.releaserc.yml` sets `tag.sign: true`, use `git tag -s` with the same notes file.
|
||||
|
||||
4. **Push if user confirmed** (Step 8):
|
||||
```bash
|
||||
@@ -380,6 +400,28 @@ After user confirmation:
|
||||
|
||||
**Note**: Do NOT add Co-Authored-By line. This is a release commit, not a code contribution.
|
||||
|
||||
### Step 10: Publish Release Artifacts and GitHub Release
|
||||
|
||||
Project artifact publishing and GitHub Releases are separate outputs:
|
||||
|
||||
1. **Project artifacts**:
|
||||
- If `release.hooks.publish_artifact` exists, run it once per prepared target
|
||||
- Pass the same `{release_notes_file}` used for the tag and GitHub Release
|
||||
- In dry-run mode, pass `{dry_run}=true` and report what would be published
|
||||
|
||||
2. **GitHub Release**:
|
||||
- Run only if the user confirmed remote publishing and GitHub support is available
|
||||
- Ensure the tag exists on the remote before creating the release
|
||||
- Create or update using the extracted notes:
|
||||
```bash
|
||||
if gh release view v{VERSION} >/dev/null 2>&1; then
|
||||
gh release edit v{VERSION} --title "v{VERSION}" --notes-file <release-notes-file>
|
||||
else
|
||||
gh release create v{VERSION} --title "v{VERSION}" --notes-file <release-notes-file> --verify-tag
|
||||
fi
|
||||
```
|
||||
- Never inline multiline release notes into shell commands
|
||||
|
||||
**Post-Release Output**:
|
||||
```
|
||||
Release v1.3.0 created.
|
||||
@@ -391,9 +433,32 @@ Commits:
|
||||
4. chore: release v1.3.0
|
||||
|
||||
Tag: v1.3.0
|
||||
Tag type: annotated
|
||||
GitHub Release: published # or "skipped/local only"
|
||||
Status: Pushed to origin # or "Local only - run git push when ready"
|
||||
```
|
||||
|
||||
## Backfill Existing GitHub Releases
|
||||
|
||||
Use this mode when the user asks to backfill historical releases or passes `--backfill-releases`.
|
||||
|
||||
1. Do not bump versions, edit changelogs, or create release commits.
|
||||
2. List existing tags in version order and detect missing releases:
|
||||
```bash
|
||||
git tag --sort=v:refname
|
||||
gh release view <tag>
|
||||
```
|
||||
3. For each tag without a GitHub Release:
|
||||
- Normalize the changelog lookup by stripping the configured tag prefix, e.g. `v1.2.3` -> `1.2.3`
|
||||
- Extract the matching section from `CHANGELOG.md`; fall back to the first matching changelog file
|
||||
- Skip or ask before publishing if no matching changelog section exists
|
||||
- Create the release with:
|
||||
```bash
|
||||
gh release create <tag> --title "<tag>" --notes-file <release-notes-file> --verify-tag
|
||||
```
|
||||
4. Detect lightweight tags with `git cat-file -t <tag>` (`commit` means lightweight, `tag` means annotated).
|
||||
5. Do not rewrite public lightweight tags by default. Converting an existing remote tag to an annotated tag requires explicit user confirmation because it rewrites a published reference.
|
||||
|
||||
## Configuration (.releaserc.yml)
|
||||
|
||||
Optional config file in project root to override defaults:
|
||||
@@ -498,6 +563,7 @@ No changes made. Run without --dry-run to execute.
|
||||
/release-skills --minor # Force minor bump
|
||||
/release-skills --patch # Force patch bump
|
||||
/release-skills --major # Force major bump (with confirmation)
|
||||
/release-skills --backfill-releases # Create missing GitHub Releases for existing tags
|
||||
```
|
||||
|
||||
## When to Use
|
||||
@@ -506,6 +572,7 @@ Trigger this skill when user requests:
|
||||
- "release", "发布", "create release", "new version", "新版本"
|
||||
- "bump version", "update version", "更新版本"
|
||||
- "prepare release"
|
||||
- "release notes", "GitHub Release", "回填 Release"
|
||||
- "push to remote" (with uncommitted changes)
|
||||
|
||||
**Important**: If user says "just push" or "直接 push" with uncommitted changes, STILL follow all steps above first.
|
||||
|
||||
+37
-1
@@ -2,6 +2,42 @@
|
||||
|
||||
English | [中文](./CHANGELOG.zh.md)
|
||||
|
||||
## 1.115.0 - 2026-05-09
|
||||
|
||||
### Features
|
||||
- `baoyu-post-to-x`: add Chrome Computer Use as the preferred execution mode in Codex. When Computer Use tools are available, all X UI actions (compose, article, quote, video) go through the user's real Chrome window instead of CDP scripts. CDP scripts become a fallback when Computer Use is unavailable or explicitly not requested.
|
||||
|
||||
## 1.114.1 - 2026-05-08
|
||||
|
||||
### Fixes
|
||||
- `baoyu-danger-gemini-web`: restore generated-image extraction for current Gemini Web responses where image URLs appear as `https://lh3.googleusercontent.com/gg-dl/` without the legacy generated-image markers. Adds regression coverage for the fallback response shape. (by @evilstar2016)
|
||||
|
||||
## 1.114.0 - 2026-05-05
|
||||
|
||||
### Features
|
||||
- `baoyu-infographic`: add `retro-popup-pop` style — retro pixel popup × pop-art collage. Renders content as a stack of 80/90s desktop popup windows (title bars, close buttons, ERROR / ALERT dialogs, file windows like `PROBLEMS.EXE`, progress bars, OK / CANCEL / FIX IT buttons) with thick black outlines, flat color fills, and bright cyan (#12B8DE) or vintage cream (#F5F0E6) backgrounds. Pairs especially well with the `dense-modules` layout; promoted as a recommended style for the `高密度信息大图` keyword shortcut and the `Product/Buying Guide` content type. Style Gallery count goes from 21 to 22.
|
||||
Credit to AJ@WaytoAGI.
|
||||
|
||||
### Documentation
|
||||
- `release-skills`: document GitHub Release publishing in the release workflow, including release-notes extraction from changelog sections, annotated tag creation, `gh release create/edit`, and historical release backfill for existing tags.
|
||||
|
||||
## 1.113.0 - 2026-04-25
|
||||
|
||||
### Features
|
||||
- `baoyu-imagine`: add DashScope Wan 2.7 image model support (`wan2.7-image-pro` and `wan2.7-image`) directly through the official Aliyun (Bailian) API. Supports text-to-image, image editing, and multi-image fusion with up to 9 reference images, with documented `[1:8, 8:1]` aspect ratio validation and per-mode pixel-budget rules. Forces `parameters.n: 1` to match baoyu-imagine's single-image save semantics and explicitly rejects `--n > 1` to prevent silent multi-image billing (the API defaults to `n=4` in non-collage mode). Allows `--provider dashscope --ref ...` opt-in for Wan 2.7 reference workflows.
|
||||
|
||||
## 1.112.0 - 2026-04-24
|
||||
|
||||
### Features
|
||||
- `baoyu-article-illustrator`: make `hand-drawn-edu` (infographic + sketch-notes + macaron) the universal fallback preset when content analysis surfaces no strong signal — warm cream paper, black hand-drawn lines, soft pastel section blocks. Elevate `sketch-notes` to primary style across infographic / flowchart / comparison / framework auto-selection; rewrite the sketch-notes style spec (macaron palette, canonical single-page layout, diagram-only rule); add matching prompt block and workflow defaults.
|
||||
- `baoyu-article-illustrator`: add `hand-drawn-edu-flow` (flowchart) and `hand-drawn-edu-compare` (comparison) presets for the same warm educational style.
|
||||
|
||||
### Breaking Changes
|
||||
- `baoyu-article-illustrator`: `hand-drawn-edu` preset now maps to `infographic` instead of `flowchart`. Users relying on the previous flowchart behavior should switch to the new `hand-drawn-edu-flow` preset.
|
||||
|
||||
### Fixes
|
||||
- `baoyu-post-to-x`: add entry point guard to `scripts/md-to-html.ts` so that importing `parseMarkdown` from `x-article.ts` no longer triggers the CLI entry point. Mirrors the same fix applied to `baoyu-post-to-weibo`.
|
||||
|
||||
## 1.111.1 - 2026-04-21
|
||||
|
||||
### Documentation
|
||||
@@ -665,7 +701,7 @@ English | [中文](./CHANGELOG.zh.md)
|
||||
- `baoyu-format-markdown`: add reader-perspective content analysis phase — analyzes highlights, structure, and formatting issues before applying formatting
|
||||
- `baoyu-format-markdown`: restructure workflow from 8 steps to 7 with explicit do/don't formatting principles and completion report
|
||||
- `baoyu-translate`: extract Step 2 workflow mechanics to separate reference file for cleaner SKILL.md
|
||||
- `baoyu-translate`: expand trigger keywords (改成中文, 快翻, 本地化, etc.) for better skill activation
|
||||
- `baoyu-translate`: expand trigger keywords (改成中文,快翻,本地化,etc.) for better skill activation
|
||||
- `baoyu-translate`: add proactive warning for long content in quick mode
|
||||
- `baoyu-translate`: save frontmatter to `chunks/frontmatter.md` during chunking
|
||||
|
||||
|
||||
+40
-4
@@ -2,6 +2,42 @@
|
||||
|
||||
[English](./CHANGELOG.md) | 中文
|
||||
|
||||
## 1.115.0 - 2026-05-09
|
||||
|
||||
### 新功能
|
||||
- `baoyu-post-to-x`:新增 Chrome Computer Use 作为 Codex 环境下的首选执行模式。当 Computer Use 工具可用时,所有 X 界面操作(发帖、文章、引用、视频)均通过用户真实 Chrome 窗口完成,不再使用 CDP 脚本。CDP 脚本降级为 Computer Use 不可用或用户明确要求时的回退方案。
|
||||
|
||||
## 1.114.1 - 2026-05-08
|
||||
|
||||
### 修复
|
||||
- `baoyu-danger-gemini-web`:修复当前 Gemini Web 响应中生成图 URL 以 `https://lh3.googleusercontent.com/gg-dl/` 形式出现、但不再包含旧版生成图 marker 时的图片提取失败问题。补充该响应形态的回归测试。 (by @evilstar2016)
|
||||
|
||||
## 1.114.0 - 2026-05-05
|
||||
|
||||
### 新功能
|
||||
- `baoyu-infographic`:新增 `retro-popup-pop` 风格 —— 复古像素弹窗 × 波普信息图。画面由多个 80/90 年代桌面弹窗叠加而成(标题栏、关闭按钮、ERROR / ALERT 报错对话框、`PROBLEMS.EXE` 等复古文件窗、进度条、OK / CANCEL / FIX IT 按钮),统一粗黑描边、平涂色块,背景使用亮青蓝(#12B8DE)或复古奶油色(#F5F0E6)。与 `dense-modules` 布局尤其契合;同时升级为 `高密度信息大图` 关键词快捷方式与 `Product/Buying Guide` 内容类型的推荐风格。风格库从 21 个扩展至 22 个。
|
||||
Credit to AJ@WaytoAGI.
|
||||
|
||||
### 文档
|
||||
- `release-skills`:补充 GitHub Release 发布流程,包括从 changelog 段落提取 release notes、创建 annotated tag、执行 `gh release create/edit`,以及为已有 tag 回填历史 GitHub Releases。
|
||||
|
||||
## 1.113.0 - 2026-04-25
|
||||
|
||||
### 新功能
|
||||
- `baoyu-imagine`:新增 DashScope Wan 2.7 图像模型支持(`wan2.7-image-pro` 与 `wan2.7-image`),通过阿里云百炼官方 API 直接调用,无需经 Replicate 转发。支持文生图、图像编辑、多图融合(最多 9 张参考图),按官方文档校验 `[1:8, 8:1]` 宽高比范围,并按模式应用不同的像素预算规则。强制 `parameters.n: 1` 以匹配 baoyu-imagine 的单图保存语义,显式拒绝 `--n > 1`,避免在用户不知情的情况下产生多图计费(API 在非拼图模式下默认 `n=4`)。允许通过 `--provider dashscope --ref ...` 显式启用 Wan 2.7 参考图工作流。
|
||||
|
||||
## 1.112.0 - 2026-04-24
|
||||
|
||||
### 新功能
|
||||
- `baoyu-article-illustrator`:当内容分析未检测到明确信号时,将 `hand-drawn-edu`(infographic + sketch-notes + macaron)作为通用默认预设 —— 暖奶油色纸面背景、黑色手绘线条、柔和马卡龙色块。`sketch-notes` 升级为 infographic / flowchart / comparison / framework 自动选择的首选风格;重写 sketch-notes 风格规范(马卡龙调色板、标准单页布局、仅限示意图的规则);新增对应的 prompt 模板块和默认工作流规则。
|
||||
- `baoyu-article-illustrator`:新增 `hand-drawn-edu-flow`(flowchart)和 `hand-drawn-edu-compare`(comparison)两个预设,保持相同的温暖教育风格。
|
||||
|
||||
### 破坏性变更
|
||||
- `baoyu-article-illustrator`:`hand-drawn-edu` 预设的类型由 `flowchart` 改为 `infographic`。依赖原有流程图行为的用户请改用新增的 `hand-drawn-edu-flow` 预设。
|
||||
|
||||
### 修复
|
||||
- `baoyu-post-to-x`:为 `scripts/md-to-html.ts` 添加入口守卫,确保 `x-article.ts` 导入 `parseMarkdown` 时不再触发 CLI 入口逻辑。与 `baoyu-post-to-weibo` 此前的修复保持一致。
|
||||
|
||||
## 1.111.1 - 2026-04-21
|
||||
|
||||
### 文档
|
||||
@@ -12,7 +48,7 @@
|
||||
|
||||
### 重构
|
||||
- 统一所有图片生成类技能(`baoyu-infographic`、`baoyu-comic`、`baoyu-cover-image`、`baoyu-image-cards`、`baoyu-article-illustrator`、`baoyu-slide-deck`、`baoyu-xhs-images`)的后端选择规则:新增单一 `preferred_image_backend` 偏好字段(`auto | ask | <backend-id>`),用 4 步解析规则(当前请求覆盖 → 已保存偏好 → 自动选择 → 询问用户)替换原有的无状态询问规则。默认优先使用运行时原生工具(如 Codex `imagegen`、Hermes `image_generate`);未设置该字段的现有 `EXTEND.md` 文件视为 `auto`,无需升级 schema 版本。
|
||||
- 在每个图片技能中新增顶级 `## Changing Preferences` 章节,作为固定后端和修改常用偏好的一级入口。
|
||||
- 在每个图片技能中新增顶级 `## Changing Preferences` 章节,作为固定后端和修改常用偏好的一级入口。
|
||||
|
||||
## 1.110.0 - 2026-04-21
|
||||
|
||||
@@ -621,7 +657,7 @@
|
||||
## 1.52.0 - 2026-03-06
|
||||
|
||||
### 新功能
|
||||
- `baoyu-post-to-weibo`:新增 `--video` 视频上传支持(图片+视频最多 18 个文件)
|
||||
- `baoyu-post-to-weibo`:新增 `--video` 视频上传支持(图片 + 视频最多 18 个文件)
|
||||
- `baoyu-post-to-weibo`:上传方式从剪贴板粘贴改为 `DOM.setFileInputFiles`,提升上传可靠性
|
||||
|
||||
### 修复
|
||||
@@ -1084,7 +1120,7 @@
|
||||
## 1.20.0 - 2026-01-24
|
||||
|
||||
### 新功能
|
||||
- `baoyu-cover-image`:从类型 × 风格二维系统升级为**四维系统**——新增 `--text` 维度(none 无文字、title-only 仅标题、title-subtitle 标题+副标题、text-rich 丰富文字)控制文字密度,新增 `--mood` 维度(subtle 低调、balanced 平衡、bold 醒目)控制情感强度。新增 `--quick` 标志跳过确认,直接使用自动选择。
|
||||
- `baoyu-cover-image`:从类型 × 风格二维系统升级为**四维系统**——新增 `--text` 维度(none 无文字、title-only 仅标题、title-subtitle 标题 + 副标题、text-rich 丰富文字)控制文字密度,新增 `--mood` 维度(subtle 低调、balanced 平衡、bold 醒目)控制情感强度。新增 `--quick` 标志跳过确认,直接使用自动选择。
|
||||
|
||||
### 文档
|
||||
- `baoyu-cover-image`:新增维度参考文件——`references/dimensions/text.md`(文字密度级别)和 `references/dimensions/mood.md`(氛围强度级别)。
|
||||
@@ -1104,7 +1140,7 @@
|
||||
- `baoyu-image-gen`:代码模块化——类型定义提取至 `types.ts`,provider 实现提取至 `providers/google.ts` 和 `providers/openai.ts`。
|
||||
|
||||
### 文档
|
||||
- `baoyu-comic`:改进 ohmsha 预设文档,明确默认哆啦A梦角色定义和视觉描述。
|
||||
- `baoyu-comic`:改进 ohmsha 预设文档,明确默认哆啦 A 梦角色定义和视觉描述。
|
||||
|
||||
## 1.18.3 - 2026-01-23
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: baoyu-article-illustrator
|
||||
description: Analyzes article structure, identifies positions requiring visual aids, generates illustrations with Type × Style × Palette three-dimension approach. Use when user asks to "illustrate article", "add images", "generate images for article", or "为文章配图".
|
||||
version: 1.57.1
|
||||
version: 1.58.0
|
||||
metadata:
|
||||
openclaw:
|
||||
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-article-illustrator
|
||||
|
||||
@@ -61,8 +61,10 @@ Position defaults to bottom-right.
|
||||
header: "Style"
|
||||
question: "Default illustration style preference? Or type another style name or your custom style"
|
||||
options:
|
||||
- label: "None (Recommended)"
|
||||
description: "Auto-select based on content analysis"
|
||||
- label: "sketch-notes (Recommended)"
|
||||
description: "Warm cream paper, black hand-drawn lines, soft pastel blocks — educational infographic feel. Great default for most articles."
|
||||
- label: "None"
|
||||
description: "Auto-select based on content analysis (falls back to sketch-notes when no strong signal)"
|
||||
- label: "notion"
|
||||
description: "Minimalist hand-drawn line art"
|
||||
- label: "warm"
|
||||
|
||||
@@ -139,6 +139,39 @@ STYLE: [style characteristics]
|
||||
ASPECT: 16:9
|
||||
```
|
||||
|
||||
**Infographic + sketch-notes + macaron palette** (default / `hand-drawn-edu` preset):
|
||||
```
|
||||
Single-page hand-drawn educational infographic in a clean presentation style.
|
||||
Warm cream paper background, black hand-drawn lines with slight wobble, soft
|
||||
pastel color blocks. Feels simple, friendly, and easy to understand at a glance.
|
||||
Diagram-style visuals ONLY — no realistic or photographic images.
|
||||
|
||||
PALETTE: macaron — soft pastel blocks on warm cream
|
||||
COLORS: Warm Cream background (#F5F0E8); Black (#1A1A1A) for ALL lines, text,
|
||||
arrows, and doodles; section fills in Light Blue (#A8D8EA), Mint Green
|
||||
(#B5E5CF), Lavender (#D5C6E0), Peach (#FFD5C2); Coral Red (#E8655A)
|
||||
sparingly for one or two emphasis points only.
|
||||
|
||||
LAYOUT (top → bottom):
|
||||
- TOP: Bold hand-lettered title, oversized, slightly wobbly, with an optional
|
||||
decorative underline or small doodle.
|
||||
- MIDDLE: 2–6 rounded-rectangle info boxes arranged in a clean grid, row, or
|
||||
radial pattern. Each box = one section, one pastel fill color, one
|
||||
simple icon or sketchy cartoon element, one short keyword/phrase.
|
||||
Hand-drawn arrows connect related zones.
|
||||
- BOTTOM: One short hand-lettered takeaway sentence summarizing the main idea.
|
||||
|
||||
ELEMENTS: Rounded info boxes with clear sectioning, wavy/straight hand-drawn
|
||||
arrows with small inline labels, simple icons and sketchy cartoon
|
||||
elements (stick figures, tools, objects), small doodle decorations
|
||||
(stars, sparkles, underlines, dots, asterisks) used sparingly.
|
||||
|
||||
STYLE: Minimal, well-organized, airy. Color fills don't completely fill
|
||||
outlines (slight "hand-painted" overshoot). ALL text hand-lettered —
|
||||
no computer fonts. Short labels and keywords only, never long
|
||||
paragraphs. Generous white space between sections.
|
||||
```
|
||||
|
||||
**Infographic + vector-illustration**:
|
||||
```
|
||||
Flat vector illustration infographic. Clean black outlines on all elements.
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
`--preset X` expands to a type + style + optional palette combination. Users can override any dimension.
|
||||
|
||||
## Default Preset
|
||||
|
||||
When content analysis surfaces no strong signal (generic knowledge article, mixed-topic post, no clear data/comparison/narrative cue), recommend **`hand-drawn-edu`** as the primary option in Step 3 Q1. It is the warm, friendly educational-infographic default — safe for most articles and universally readable.
|
||||
|
||||
## By Category
|
||||
|
||||
### Technical & Engineering
|
||||
@@ -23,7 +27,9 @@
|
||||
| `process-flow` | `flowchart` | `notion` | — | Workflow documentation, onboarding flows |
|
||||
| `warm-knowledge` | `infographic` | `vector-illustration` | `warm` | Product showcases, team intros, feature cards, brand content |
|
||||
| `edu-visual` | `infographic` | `vector-illustration` | `macaron` | Knowledge summaries, concept explainers, educational articles |
|
||||
| `hand-drawn-edu` | `flowchart` | `sketch-notes` | `macaron` | Hand-drawn educational diagrams, process explainers, onboarding visuals |
|
||||
| `hand-drawn-edu` | `infographic` | `sketch-notes` | `macaron` | **Default preset.** Hand-drawn educational infographic — warm cream paper, black lines, pastel blocks. Great for single-page explainers, concept summaries, onboarding, general knowledge articles |
|
||||
| `hand-drawn-edu-flow` | `flowchart` | `sketch-notes` | `macaron` | Hand-drawn process explainer — step-by-step workflow in the same warm educational style |
|
||||
| `hand-drawn-edu-compare` | `comparison` | `sketch-notes` | `macaron` | Hand-drawn side-by-side comparison in the warm educational style |
|
||||
| `ink-notes-compare` | `comparison` | `ink-notes` | `mono-ink` | Before/After essays, Traditional vs New, OS-style comparisons, mindset-shift narratives |
|
||||
| `ink-notes-flow` | `flowchart` | `ink-notes` | `mono-ink` | Professional process explainers, workforce pipelines, hand-drawn technical walkthroughs |
|
||||
| `ink-notes-framework` | `framework` | `ink-notes` | `mono-ink` | System analogies, command-center diagrams, architecture-as-metaphor, tech manifestos |
|
||||
@@ -59,18 +65,19 @@ Use this table during Step 3 to recommend presets based on Step 2 content analys
|
||||
|
||||
| Content Type (Step 2) | Primary Preset | Alternatives |
|
||||
|------------------------|----------------|--------------|
|
||||
| Technical | `tech-explainer` | `system-design`, `architecture` |
|
||||
| Tutorial | `tutorial` | `process-flow`, `knowledge-base`, `edu-visual` |
|
||||
| **General / No strong signal** | `hand-drawn-edu` | `edu-visual`, `knowledge-base` |
|
||||
| Education / Knowledge | `hand-drawn-edu` | `edu-visual`, `knowledge-base`, `tutorial` |
|
||||
| Tutorial | `hand-drawn-edu-flow` | `tutorial`, `process-flow`, `hand-drawn-edu` |
|
||||
| SaaS / Product | `hand-drawn-edu` | `saas-guide`, `knowledge-base`, `process-flow`, `warm-knowledge` |
|
||||
| Technical | `tech-explainer` | `system-design`, `architecture`, `hand-drawn-edu` |
|
||||
| Methodology / Framework | `system-design` | `architecture`, `process-flow` |
|
||||
| Data / Metrics | `data-report` | `versus`, `tech-explainer` |
|
||||
| Comparison / Review | `versus` | `business-compare`, `editorial-poster`, `ink-notes-compare` |
|
||||
| Comparison / Review | `versus` | `business-compare`, `hand-drawn-edu-compare`, `editorial-poster`, `ink-notes-compare` |
|
||||
| Manifesto / Mindset shift / Professional visual note | `ink-notes-compare` | `ink-notes-framework`, `ink-notes-flow` |
|
||||
| Narrative / Personal | `storytelling` | `lifestyle`, `evolution` |
|
||||
| Opinion / Editorial | `opinion-piece` | `cinematic`, `editorial-poster` |
|
||||
| Historical / Timeline | `history` | `evolution` |
|
||||
| Academic / Research | `science-paper` | `tech-explainer`, `data-report` |
|
||||
| SaaS / Product | `saas-guide` | `knowledge-base`, `process-flow`, `warm-knowledge` |
|
||||
| Education / Knowledge | `edu-visual` | `knowledge-base`, `tutorial`, `hand-drawn-edu` |
|
||||
|
||||
## Override Examples
|
||||
|
||||
|
||||
@@ -6,15 +6,15 @@ Simplified style tier for quick selection:
|
||||
|
||||
| Core Style | Maps To | Best For |
|
||||
|------------|---------|----------|
|
||||
| `hand-drawn` | sketch-notes | **Default.** Warm cream paper, black hand-drawn lines, pastel blocks — educational infographics, concept explainers, onboarding, general knowledge articles |
|
||||
| `vector` | vector-illustration | Knowledge articles, tutorials, tech content |
|
||||
| `minimal-flat` | notion | General, knowledge sharing, SaaS |
|
||||
| `sci-fi` | blueprint | AI, frontier tech, system design |
|
||||
| `hand-drawn` | sketch/warm | Relaxed, reflective, casual content |
|
||||
| `editorial` | editorial | Processes, data, journalism |
|
||||
| `scene` | warm/watercolor | Narratives, emotional, lifestyle |
|
||||
| `poster` | screen-print | Opinion, editorial, cultural, cinematic |
|
||||
|
||||
Use Core Styles for most cases. See full Style Gallery below for granular control.
|
||||
Use Core Styles for most cases. **When no strong content signal is detected, default to `hand-drawn` (→ sketch-notes).** See full Style Gallery below for granular control.
|
||||
|
||||
---
|
||||
|
||||
@@ -50,42 +50,45 @@ Full specifications: `references/styles/<style>.md`
|
||||
|
||||
## Type × Style Compatibility Matrix
|
||||
|
||||
| | vector-illustration | notion | warm | minimal | blueprint | watercolor | elegant | editorial | scientific | screen-print |
|
||||
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||
| infographic | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓✓ | ✓ |
|
||||
| scene | ✓ | ✓ | ✓✓ | ✓ | ✗ | ✓✓ | ✓ | ✓ | ✗ | ✓✓ |
|
||||
| flowchart | ✓✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✗ | ✓ | ✓✓ | ✓ | ✗ |
|
||||
| comparison | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓ | ✓ |
|
||||
| framework | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✗ | ✓✓ | ✓ | ✓✓ | ✓ |
|
||||
| timeline | ✓ | ✓✓ | ✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓ |
|
||||
| | sketch-notes | vector-illustration | notion | warm | minimal | blueprint | watercolor | elegant | editorial | scientific | screen-print |
|
||||
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||
| infographic | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓✓ | ✓ |
|
||||
| scene | ✗ | ✓ | ✓ | ✓✓ | ✓ | ✗ | ✓✓ | ✓ | ✓ | ✗ | ✓✓ |
|
||||
| flowchart | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✗ | ✓ | ✓✓ | ✓ | ✗ |
|
||||
| comparison | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓ | ✓ |
|
||||
| framework | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✗ | ✓✓ | ✓ | ✓✓ | ✓ |
|
||||
| timeline | ✓ | ✓ | ✓✓ | ✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓ |
|
||||
|
||||
✓✓ = highly recommended | ✓ = compatible | ✗ = not recommended
|
||||
|
||||
## Auto Selection by Type
|
||||
|
||||
When no content signal matches strongly, `sketch-notes` is the default primary for every diagrammatic type. Only override with another primary when the content analysis in Step 2 surfaces a clear signal (technical/data/narrative/opinion).
|
||||
|
||||
| Type | Primary Style | Secondary Styles |
|
||||
|------|---------------|------------------|
|
||||
| infographic | vector-illustration | notion, blueprint, editorial |
|
||||
| infographic | sketch-notes | vector-illustration, notion, blueprint, editorial |
|
||||
| scene | warm | watercolor, elegant |
|
||||
| flowchart | vector-illustration | notion, blueprint |
|
||||
| comparison | vector-illustration | notion, elegant |
|
||||
| framework | blueprint | vector-illustration, notion |
|
||||
| timeline | elegant | warm, editorial |
|
||||
| flowchart | sketch-notes | vector-illustration, notion, blueprint |
|
||||
| comparison | sketch-notes | vector-illustration, notion, elegant |
|
||||
| framework | sketch-notes | blueprint, vector-illustration, notion |
|
||||
| timeline | elegant | sketch-notes, warm, editorial |
|
||||
|
||||
## Auto Selection by Content Signals
|
||||
|
||||
| Content Signals | Recommended Type | Recommended Style |
|
||||
|-----------------|------------------|-------------------|
|
||||
| **(no strong signal / general article)** | **infographic** | **sketch-notes** |
|
||||
| Knowledge, concept, tutorial, learning, guide, onboarding | infographic | sketch-notes, vector-illustration, notion |
|
||||
| Productivity, SaaS, tool, app, software | infographic | sketch-notes, notion, vector-illustration |
|
||||
| How-to, steps, workflow, process, tutorial | flowchart | sketch-notes, vector-illustration, notion |
|
||||
| API, metrics, data, comparison, numbers | infographic | blueprint, vector-illustration |
|
||||
| Knowledge, concept, tutorial, learning, guide | infographic | vector-illustration, notion |
|
||||
| Tech, AI, programming, development, code | infographic | vector-illustration, blueprint |
|
||||
| How-to, steps, workflow, process, tutorial | flowchart | vector-illustration, notion |
|
||||
| Framework, model, architecture, principles | framework | blueprint, vector-illustration |
|
||||
| vs, pros/cons, before/after, alternatives | comparison | vector-illustration, notion |
|
||||
| Tech, AI, programming, development, code | infographic | vector-illustration, blueprint, sketch-notes |
|
||||
| Framework, model, architecture, principles | framework | blueprint, vector-illustration, sketch-notes |
|
||||
| vs, pros/cons, before/after, alternatives | comparison | vector-illustration, notion, sketch-notes |
|
||||
| Manifesto, mindset shift, workforce, OS, whiteboard, professional visual note | comparison / framework | ink-notes |
|
||||
| Story, emotion, journey, experience, personal | scene | warm, watercolor |
|
||||
| History, timeline, progress, evolution | timeline | elegant, warm |
|
||||
| Productivity, SaaS, tool, app, software | infographic | notion, vector-illustration |
|
||||
| Business, professional, strategy, corporate | framework | elegant |
|
||||
| Opinion, editorial, culture, philosophy, cinematic, dramatic, poster | scene | screen-print |
|
||||
| Biology, chemistry, medical, scientific | infographic | scientific |
|
||||
@@ -93,6 +96,15 @@ Full specifications: `references/styles/<style>.md`
|
||||
|
||||
## Style Characteristics by Type
|
||||
|
||||
### infographic + sketch-notes (default)
|
||||
- Warm cream paper background, black hand-drawn lines with slight wobble
|
||||
- 2–6 rounded pastel info boxes (light blue / mint / lavender / peach)
|
||||
- Bold hand-lettered title at the top
|
||||
- Short keyword labels, simple icons, small doodles (stars, underlines, sparkles)
|
||||
- One-line hand-lettered takeaway sentence at the bottom
|
||||
- Airy, minimal, diagram-style — never realistic
|
||||
- Perfect for single-page educational explainers and concept summaries
|
||||
|
||||
### infographic + vector-illustration
|
||||
- Clean flat vector shapes, bold geometric forms
|
||||
- Vibrant but harmonious color palette
|
||||
|
||||
@@ -1,56 +1,91 @@
|
||||
# sketch-notes
|
||||
|
||||
Soft hand-drawn illustration style with warm, educational feel
|
||||
Hand-drawn educational infographic style with warm cream paper, black hand-drawn lines, and soft pastel section blocks. Optimized for single-page visual explainers.
|
||||
|
||||
## Design Aesthetic
|
||||
|
||||
Hand-drawn feel with soft, relaxed brush strokes. Fresh, refined style with minimalist editorial approach. Emphasis on precision, clarity and intelligent elegance while prioritizing warmth, approachability and friendliness.
|
||||
Hand-drawn educational infographic in a clean presentation style. Feels like a visual explainer slide: simple, friendly, and easy to understand at a glance. Bold handwritten-style title at the top, clearly sectioned content in the middle with rounded boxes and small doodles, and one short takeaway sentence at the bottom. Neat, airy, and visually similar to a hand-drawn concept diagram — never realistic or photographic.
|
||||
|
||||
## Background
|
||||
|
||||
- Color: Warm Off-White (#FAF8F0)
|
||||
- Texture: Subtle paper grain, warm tone
|
||||
- Color: Warm Cream Paper (#F5F0E8) — preferred; fallback Warm Off-White (#FAF8F0)
|
||||
- Texture: Subtle warm paper grain, matte finish, no gloss
|
||||
|
||||
## Color Palette
|
||||
|
||||
Default sketch-notes palette is the **macaron** pastel set. Lines are always black; pastel blocks are used only as rounded card fills for information sections.
|
||||
|
||||
| Role | Color | Hex | Usage |
|
||||
|------|-------|-----|-------|
|
||||
| Background | Warm Off-White | #FAF8F0 | Primary background |
|
||||
| Primary Text | Deep Charcoal | #2C3E50 | Main elements |
|
||||
| Alt Text | Deep Brown | #4A4A4A | Secondary elements |
|
||||
| Accent 1 | Soft Orange | #F4A261 | Highlights, emphasis |
|
||||
| Accent 2 | Mustard Yellow | #E9C46A | Secondary highlights |
|
||||
| Accent 3 | Sage Green | #87A96B | Nature, growth concepts |
|
||||
| Accent 4 | Light Blue | #7EC8E3 | Tech, digital elements |
|
||||
| Accent 5 | Red Brown | #A0522D | Earthy elements |
|
||||
| Background | Warm Cream | #F5F0E8 | Paper background |
|
||||
| Primary Ink | Black | #1A1A1A | ALL outlines, text, arrows, doodles |
|
||||
| Block Blue | Light Blue | #A8D8EA | Info block fill (cool / tech) |
|
||||
| Block Mint | Mint Green | #B5E5CF | Info block fill (growth / positive) |
|
||||
| Block Lavender | Lavender | #D5C6E0 | Info block fill (concept / abstract) |
|
||||
| Block Peach | Peach | #FFD5C2 | Info block fill (warm / human) |
|
||||
| Accent | Coral Red | #E8655A | One or two emphasis points only |
|
||||
| Muted Text | Warm Gray | #6B6B6B | Small annotations |
|
||||
|
||||
Use **4 pastel block colors max** per image, one color per section. Black ink does all the structural line work.
|
||||
|
||||
## Visual Elements
|
||||
|
||||
- Connection lines with hand-drawn wavy feel
|
||||
- Conceptual abstract icons illustrating ideas
|
||||
- Color fills don't completely fill outlines (hand-painted feel)
|
||||
- Simple geometric shapes with rounded corners
|
||||
- Arrows and pointers with sketchy style
|
||||
- Doodle decorations: stars, spirals, underlines
|
||||
- Bold hand-lettered title at the top (oversized, slightly wobbly)
|
||||
- Rounded-rectangle info boxes with clear sectioning (2–6 zones)
|
||||
- Short keyword labels inside boxes — never long paragraphs
|
||||
- Simple icons and sketchy cartoon elements (stick figures, tools, objects) to explain each idea
|
||||
- Hand-drawn arrows (straight, curved, or wavy) connecting related zones
|
||||
- Small doodle decorations: stars, sparkles, underlines, dots, asterisks — used sparingly for emphasis
|
||||
- Single-line hand-lettered takeaway sentence at the bottom
|
||||
- Color fills do not completely fill outlines (slight "hand-painted" overshoot/undershoot)
|
||||
- Generous white space between sections — airy, never crowded
|
||||
|
||||
## Layout Guidelines
|
||||
|
||||
Canonical single-page layout (16:9 or 4:3):
|
||||
|
||||
1. **Top (10–15%)** — Bold hand-lettered title, optionally with a small decorative underline or doodle.
|
||||
2. **Middle (70–80%)** — 2–6 rounded pastel info boxes arranged in a clear grid, row, or radial pattern. Each box = one section, one color, one icon, one keyword/phrase.
|
||||
3. **Bottom (10–15%)** — One short hand-lettered takeaway sentence summarizing the core insight.
|
||||
|
||||
Keep margins generous. Aim for breathing room around every element.
|
||||
|
||||
## Style Rules
|
||||
|
||||
### Do
|
||||
|
||||
- Keep layouts open and well-structured
|
||||
- Emphasize information hierarchy
|
||||
- Use hand-drawn quality for all elements
|
||||
- Allow imperfection (slight wobbles add character)
|
||||
- Layer elements with subtle overlaps
|
||||
- Use warm cream paper background (no pure white)
|
||||
- Use black hand-drawn lines for ALL structural elements
|
||||
- Use soft pastel blocks (blue / mint / lavender / peach) for section fills
|
||||
- Keep text to short keywords and phrases only
|
||||
- Include a bold handwritten title at the top
|
||||
- Include a short takeaway sentence at the bottom
|
||||
- Use diagram-style visuals (icons, doodles, simple shapes)
|
||||
- Allow slight wobble — hand-drawn imperfection is the point
|
||||
- Maintain clear sectioning with rounded boxes
|
||||
|
||||
### Don't
|
||||
|
||||
- Use perfect geometric shapes
|
||||
- Create photorealistic elements
|
||||
- Overcrowd with too many elements
|
||||
- Use pure white backgrounds
|
||||
- Make it look computer-generated
|
||||
- Use pure white backgrounds (that's `ink-notes`' territory)
|
||||
- Render realistic or photographic images — this style is diagram-only
|
||||
- Fill zones with gradients, shadows, or digital effects
|
||||
- Use long paragraphs of text — keywords only
|
||||
- Use computer-generated / sans-serif body fonts — ALL text must be hand-lettered
|
||||
- Use more than 4 pastel block colors per image
|
||||
- Overcrowd the canvas — keep it airy and minimal
|
||||
- Use perfect geometric shapes — preserve the hand-drawn wobble
|
||||
|
||||
## Type Compatibility
|
||||
|
||||
| Type | Rating | Notes |
|
||||
|------|--------|-------|
|
||||
| infographic | ✓✓ | **Best fit** — single-page visual explainers, concept summaries, educational slides |
|
||||
| framework | ✓✓ | Labeled zones and connectors render well |
|
||||
| flowchart | ✓✓ | Rounded step boxes with wavy arrows |
|
||||
| comparison | ✓✓ | Two pastel blocks side by side; prefer `ink-notes` for strict Before/After contrasts |
|
||||
| timeline | ✓ | Hand-drawn horizontal arrow with milestone cards |
|
||||
| scene | ✗ | Not recommended — too diagrammatic |
|
||||
|
||||
## Best For
|
||||
|
||||
Educational content, knowledge sharing, technical explanations, tutorials, onboarding materials, friendly articles
|
||||
Educational content, knowledge sharing, concept explainers, tutorials, onboarding materials, product walkthroughs, single-page visual summaries, "how things work" posts, friendly technical articles
|
||||
|
||||
@@ -176,6 +176,8 @@ Based on Step 2 content analysis, recommend a preset first (sets both type & sty
|
||||
- [Alternative preset] — [brief]
|
||||
- Or choose type manually: infographic / scene / flowchart / comparison / framework / timeline / mixed
|
||||
|
||||
**Default**: if Step 2 found no strong content signal, the recommended preset MUST be `hand-drawn-edu` (infographic + sketch-notes + macaron — warm cream paper, black hand-drawn lines, soft pastel blocks). This is the universal fallback.
|
||||
|
||||
**If user picks a preset → skip Q3** (type & style both resolved).
|
||||
**If user picks a type → Q3 is REQUIRED.**
|
||||
|
||||
@@ -203,13 +205,15 @@ If no `preferred_style` (present Core Styles first):
|
||||
|
||||
| Core Style | Maps To | Best For |
|
||||
|------------|---------|----------|
|
||||
| `hand-drawn` | sketch-notes | **Default.** Warm cream paper, black hand-drawn lines, pastel blocks — educational infographics, concept explainers, onboarding, general knowledge articles |
|
||||
| `minimal-flat` | notion | General, knowledge sharing, SaaS |
|
||||
| `sci-fi` | blueprint | AI, frontier tech, system design |
|
||||
| `hand-drawn` | sketch/warm | Relaxed, reflective, casual |
|
||||
| `editorial` | editorial | Processes, data, journalism |
|
||||
| `scene` | warm/watercolor | Narratives, emotional, lifestyle |
|
||||
| `poster` | screen-print | Opinion, editorial, cultural, cinematic |
|
||||
|
||||
**Default recommendation**: when Step 2 surfaces no strong content signal, recommend **`hand-drawn-edu`** preset (→ infographic + sketch-notes + macaron) as the primary option in Q1. When the user picks a type manually without a preferred_style, recommend `sketch-notes` first in Q3.
|
||||
|
||||
Style selection based on Type × Style compatibility matrix (styles.md).
|
||||
**In Step 5.1**, read `styles/<style>.md` for visual elements and rendering rules.
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { collect_generated_image_urls_from_response_parts } from "./client.ts";
|
||||
|
||||
test("response part fallback finds generated images when legacy generated markers are absent", () => {
|
||||
const generatedUrl = "https://lh3.googleusercontent.com/gg-dl/example-generated-image";
|
||||
const initialCandidate = ["rcid-1", ["image generated successfully"]];
|
||||
const imageCandidate = [
|
||||
"rcid-1",
|
||||
["image generated successfully"],
|
||||
{ nestedPayload: [{ media: generatedUrl }] },
|
||||
];
|
||||
const responseJson = [
|
||||
["wrb.fr", null, JSON.stringify([null, [], null, null, [initialCandidate]])],
|
||||
["wrb.fr", null, JSON.stringify([null, [], null, null, [imageCandidate]])],
|
||||
];
|
||||
|
||||
assert.equal(initialCandidate[12], undefined);
|
||||
assert.equal(
|
||||
/http:\/\/googleusercontent\.com\/image_generation_content\/\d+/.test(String(initialCandidate[1]?.[0])),
|
||||
false,
|
||||
);
|
||||
assert.deepEqual(collect_generated_image_urls_from_response_parts(responseJson, 0, 0), [generatedUrl]);
|
||||
});
|
||||
@@ -37,6 +37,8 @@ type InitOptions = {
|
||||
|
||||
type RequestKwargs = RequestInit & { timeout_ms?: number };
|
||||
|
||||
const GENERATED_IMAGE_URL_PREFIX = 'https://lh3.googleusercontent.com/gg-dl/';
|
||||
|
||||
function normalize_headers(h?: HeadersInit): Record<string, string> {
|
||||
if (!h) return {};
|
||||
if (Array.isArray(h)) return Object.fromEntries(h.map(([k, v]) => [k, v]));
|
||||
@@ -78,6 +80,59 @@ function collect_strings(root: unknown, accept: (s: string) => boolean, limit: n
|
||||
return out;
|
||||
}
|
||||
|
||||
function collect_generated_image_urls(root: unknown, limit: number = 4): string[] {
|
||||
return collect_strings(root, (s) => s.startsWith(GENERATED_IMAGE_URL_PREFIX), limit);
|
||||
}
|
||||
|
||||
function parse_response_part_body(part: unknown): unknown[] | null {
|
||||
if (!Array.isArray(part)) return null;
|
||||
const part_body = get_nested_value<string | null>(part, [2], null);
|
||||
if (!part_body) return null;
|
||||
try {
|
||||
const part_json = JSON.parse(part_body) as unknown;
|
||||
return Array.isArray(part_json) ? part_json : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function find_generated_image_part(
|
||||
response_json: unknown[],
|
||||
body_index: number,
|
||||
candidate_index: number,
|
||||
limit: number = 4,
|
||||
): { body: unknown[]; urls: string[] } | null {
|
||||
for (let part_index = body_index; part_index < response_json.length; part_index++) {
|
||||
const part_json = parse_response_part_body(response_json[part_index]);
|
||||
if (!part_json) continue;
|
||||
const cand = get_nested_value<unknown>(part_json, [4, candidate_index], null);
|
||||
if (!cand) continue;
|
||||
const urls = collect_generated_image_urls(cand, limit);
|
||||
if (urls.length > 0) return { body: part_json, urls };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function collect_generated_image_urls_from_response_parts(
|
||||
response_json: unknown[],
|
||||
body_index: number,
|
||||
candidate_index: number,
|
||||
limit: number = 4,
|
||||
): string[] {
|
||||
return find_generated_image_part(response_json, body_index, candidate_index, limit)?.urls ?? [];
|
||||
}
|
||||
|
||||
function push_generated_images(
|
||||
generated_images: GeneratedImage[],
|
||||
urls: string[],
|
||||
proxy: string | null,
|
||||
cookies: Record<string, string>,
|
||||
): void {
|
||||
for (const url of urls) {
|
||||
generated_images.push(new GeneratedImage(url, '[Generated Image]', '', proxy, cookies));
|
||||
}
|
||||
}
|
||||
|
||||
export class GeminiClient extends GemMixin {
|
||||
public cookies: Record<string, string> = {};
|
||||
public proxy: string | null = null;
|
||||
@@ -404,24 +459,8 @@ export class GeminiClient extends GemMixin {
|
||||
/http:\/\/googleusercontent\.com\/image_generation_content\/\d+/.test(text);
|
||||
|
||||
if (wants_generated) {
|
||||
let img_body: unknown[] | null = null;
|
||||
for (let part_index = body_index; part_index < (response_json as unknown[]).length; part_index++) {
|
||||
const part = (response_json as unknown[])[part_index];
|
||||
if (!Array.isArray(part)) continue;
|
||||
const part_body = get_nested_value<string | null>(part, [2], null);
|
||||
if (!part_body) continue;
|
||||
try {
|
||||
const part_json = JSON.parse(part_body) as unknown[];
|
||||
const cand = get_nested_value<unknown>(part_json, [4, candidate_index], null);
|
||||
if (!cand) continue;
|
||||
|
||||
const urls = collect_strings(cand, (s) => s.startsWith('https://lh3.googleusercontent.com/gg-dl/'), 1);
|
||||
if (urls.length > 0) {
|
||||
img_body = part_json;
|
||||
break;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
const image_part = find_generated_image_part(response_json as unknown[], body_index, candidate_index, 1);
|
||||
const img_body = image_part?.body ?? null;
|
||||
|
||||
if (!img_body) {
|
||||
throw new ImageGenerationError(
|
||||
@@ -452,13 +491,22 @@ export class GeminiClient extends GemMixin {
|
||||
}
|
||||
|
||||
if (generated_images.length === 0) {
|
||||
const urls = collect_strings(img_candidate, (s) => s.startsWith('https://lh3.googleusercontent.com/gg-dl/'), 4);
|
||||
for (const url of urls) {
|
||||
generated_images.push(new GeneratedImage(url, '[Generated Image]', '', this.proxy, this.cookies));
|
||||
}
|
||||
push_generated_images(generated_images, collect_generated_image_urls(img_candidate), this.proxy, this.cookies);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: unconditionally scan all response parts for generated image URLs.
|
||||
// The `wants_generated` detection above relies on `candidate[12][7][0]` and an old
|
||||
// `googleusercontent.com/image_generation_content/` URL pattern, both of which no
|
||||
// longer appear in the current Gemini Web API response format.
|
||||
// When Gemini does generate images, their URLs now start with
|
||||
// `https://lh3.googleusercontent.com/gg-dl/` and are present somewhere in the
|
||||
// response parts — this fallback finds them so images are not silently dropped.
|
||||
if (generated_images.length === 0) {
|
||||
const urls = collect_generated_image_urls_from_response_parts(response_json as unknown[], body_index, candidate_index);
|
||||
push_generated_images(generated_images, urls, this.proxy, this.cookies);
|
||||
}
|
||||
|
||||
out.push(new Candidate({ rcid, text, thoughts, web_images, generated_images }));
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ ${BUN_X} {baseDir}/scripts/main.ts --batchfile batch.json --jobs 4
|
||||
| `--quality normal\|2k` | Quality preset (default: `2k`) |
|
||||
| `--imageSize 1K\|2K\|4K` | Image size for Google/OpenRouter (default: from quality) |
|
||||
| `--imageApiDialect openai-native\|ratio-metadata` | OpenAI-compatible endpoint dialect — use `ratio-metadata` for gateways that expect aspect-ratio `size` plus `metadata.resolution` |
|
||||
| `--ref <files...>` | Reference images. Supported by Google multimodal, OpenAI GPT Image edits, Azure OpenAI edits (PNG/JPG only), OpenRouter multimodal models, Replicate supported families, MiniMax subject-reference, Seedream 5.0/4.5/4.0. Not supported by Jimeng, Seedream 3.0, SeedEdit 3.0 |
|
||||
| `--ref <files...>` | Reference images. Supported by Google multimodal, OpenAI GPT Image edits, Azure OpenAI edits (PNG/JPG only), OpenRouter multimodal models, Replicate supported families, MiniMax subject-reference, Seedream 5.0/4.5/4.0, DashScope `wan2.7-image-pro`/`wan2.7-image`. Not supported by Jimeng, Seedream 3.0, SeedEdit 3.0, or any DashScope model outside the `wan2.7-image*` family |
|
||||
| `--n <count>` | Number of images. Replicate requires `--n 1` (single-output save semantics) |
|
||||
| `--json` | JSON output |
|
||||
|
||||
|
||||
@@ -271,6 +271,10 @@ options:
|
||||
description: "Legacy Qwen model with five fixed output sizes"
|
||||
- label: "qwen-image-plus"
|
||||
description: "Legacy Qwen model, same current capability as qwen-image"
|
||||
- label: "wan2.7-image-pro"
|
||||
description: "Wan 2.7 Pro — supports up to 4K text-to-image and reference-image editing"
|
||||
- label: "wan2.7-image"
|
||||
description: "Wan 2.7 base — faster generation, up to 2K, supports reference-image editing"
|
||||
- label: "z-image-turbo"
|
||||
description: "Legacy DashScope model for compatibility"
|
||||
- label: "z-image-ultra"
|
||||
@@ -281,6 +285,7 @@ Notes for DashScope setup:
|
||||
|
||||
- Prefer `qwen-image-2.0-pro` when the user needs custom `--size`, uncommon ratios like `21:9`, or strong Chinese/English text rendering.
|
||||
- `qwen-image-max` / `qwen-image-plus` / `qwen-image` only support five fixed sizes: `1664*928`, `1472*1104`, `1328*1328`, `1104*1472`, `928*1664`.
|
||||
- `wan2.7-image-pro` and `wan2.7-image` are the only DashScope models that accept `--ref`. Pick one of these when the user wants reference-image editing or multi-image fusion via DashScope.
|
||||
- In `baoyu-imagine`, `quality` is a compatibility preset. It is not a native DashScope parameter.
|
||||
|
||||
### Z.AI Model Selection
|
||||
|
||||
@@ -17,6 +17,17 @@ Read when the user picks `--provider dashscope`, sets `default_model.dashscope`,
|
||||
- Default is `1664*928`
|
||||
- `qwen-image` currently has the same capability as `qwen-image-plus`
|
||||
|
||||
**`wan2.7-image*`** — multimodal Wan 2.7 family. Members: `wan2.7-image-pro`, `wan2.7-image`.
|
||||
|
||||
- Free-form `size` in `宽*高` format, plus aspect-ratio inference
|
||||
- `wan2.7-image-pro` text-to-image (no `--ref`): total pixels in `[768*768, 4096*4096]`, ratio in `[1:8, 8:1]`
|
||||
- `wan2.7-image-pro` with reference images and `wan2.7-image` (all scenarios): total pixels in `[768*768, 2048*2048]`, ratio in `[1:8, 8:1]`
|
||||
- Default: `1024*1024` (`--quality normal`) or `2048*2048` (`--quality 2k`); 4K requires explicit `--size`
|
||||
- Supports up to 9 reference images in `--ref` (image editing / multi-image fusion)
|
||||
- Reference images are sent inline as base64 (or passed through if the path is an `http(s)://` URL)
|
||||
- API does NOT use `prompt_extend`; the skill omits it for this family
|
||||
- The Wan 2.7 API defaults `n` to **4** in non-collage mode and bills per generated image. baoyu-imagine forces `n: 1` and rejects `--n > 1` to avoid silently paying for and discarding extra images.
|
||||
|
||||
**Legacy** — `z-image-turbo`, `z-image-ultra`, `wanx-v1`. Only use when the user explicitly asks for legacy behavior.
|
||||
|
||||
## Size Resolution
|
||||
@@ -24,7 +35,8 @@ Read when the user picks `--provider dashscope`, sets `default_model.dashscope`,
|
||||
- `--size` wins over `--ar`
|
||||
- For `qwen-image-2.0*`: prefer explicit `--size`; otherwise infer from `--ar` using the recommended table below
|
||||
- For `qwen-image-max/plus/image`: only use the five fixed sizes; if the requested ratio doesn't fit, switch to `qwen-image-2.0-pro`
|
||||
- `--quality` is a baoyu-imagine preset, not an official DashScope field. The mapping of `normal`/`2k` onto the `qwen-image-2.0*` table is an implementation choice, not an API guarantee
|
||||
- For `wan2.7-image*`: explicit `--size` is validated against the per-mode pixel/ratio limits; otherwise the size is derived from `--ar` and `--quality` (`normal` ≈ 1K, `2k` ≈ 2K). To request 4K with `wan2.7-image-pro` text-to-image, pass `--size` explicitly (e.g. `4096*4096`, `3840*2160`)
|
||||
- `--quality` is a baoyu-imagine preset, not an official DashScope field. The mapping of `normal`/`2k` onto the `qwen-image-2.0*` and `wan2.7-image*` tables is an implementation choice, not an API guarantee
|
||||
|
||||
### Recommended `qwen-image-2.0*` sizes
|
||||
|
||||
@@ -39,12 +51,19 @@ Read when the user picks `--provider dashscope`, sets `default_model.dashscope`,
|
||||
| `16:9` | `1280*720` | `1920*1080` |
|
||||
| `21:9` | `1344*576` | `2048*872` |
|
||||
|
||||
## Reference Images
|
||||
|
||||
- Only `wan2.7-image-pro` and `wan2.7-image` accept `--ref`. Other DashScope models (qwen-image-2.0*, qwen-image-max/plus/image, legacy) reject `--ref` and the user is steered to a different provider/model.
|
||||
- Up to 9 reference images per request. Local files are inlined as base64 data URLs; `http(s)://` URLs are forwarded as-is.
|
||||
- Supplying any `--ref` automatically clamps the wan2.7-image-pro pixel ceiling from 4K to 2K (the API only supports 4K for pure text-to-image with no image input).
|
||||
|
||||
## Not Exposed
|
||||
|
||||
DashScope APIs also support `negative_prompt`, `prompt_extend`, and `watermark`, but `baoyu-imagine` does not expose them as CLI flags today.
|
||||
DashScope APIs also support `negative_prompt`, `prompt_extend`, `watermark`, `thinking_mode`, `seed`, `bbox_list`, `enable_sequential`, and `color_palette`. `baoyu-imagine` does not expose them as CLI flags today; the wan2.7 family relies on the API defaults (e.g. `thinking_mode=true`). The skill always sends `n=1` for wan2.7 — if you want grid/collage mode you currently need to call the API directly.
|
||||
|
||||
## Official References
|
||||
|
||||
- [Qwen-Image API](https://help.aliyun.com/zh/model-studio/qwen-image-api)
|
||||
- [Text-to-image guide](https://help.aliyun.com/zh/model-studio/text-to-image)
|
||||
- [Qwen-Image Edit API](https://help.aliyun.com/zh/model-studio/qwen-image-edit-api)
|
||||
- [Wan 2.7 image generation & editing API](https://help.aliyun.com/zh/model-studio/wan-image-generation-and-editing-api-reference)
|
||||
|
||||
@@ -51,6 +51,12 @@ ${BUN_X} {baseDir}/scripts/main.ts --prompt "为咖啡品牌设计一张 21:9
|
||||
# DashScope legacy fixed-size
|
||||
${BUN_X} {baseDir}/scripts/main.ts --prompt "一张电影感海报" --image out.png --provider dashscope --model qwen-image-max --size 1664x928
|
||||
|
||||
# DashScope Wan 2.7 Image Pro (4K text-to-image)
|
||||
${BUN_X} {baseDir}/scripts/main.ts --prompt "一间有着精致窗户的花店" --image out.png --provider dashscope --model wan2.7-image-pro --size 4096x4096
|
||||
|
||||
# DashScope Wan 2.7 Image with reference image (multi-image fusion)
|
||||
${BUN_X} {baseDir}/scripts/main.ts --prompt "把图2的涂鸦喷绘在图1的汽车上" --image out.png --provider dashscope --model wan2.7-image-pro --ref car.webp paint.webp
|
||||
|
||||
# Z.AI GLM-image
|
||||
${BUN_X} {baseDir}/scripts/main.ts --prompt "一张带清晰中文标题的科技海报" --image out.png --provider zai
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
parseArgs,
|
||||
parseOpenAIImageApiDialect,
|
||||
parseSimpleYaml,
|
||||
validateReferenceImages,
|
||||
} from "./main.ts";
|
||||
|
||||
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
@@ -123,6 +124,15 @@ test("parseArgs falls back to positional prompt and rejects invalid provider", (
|
||||
);
|
||||
});
|
||||
|
||||
test("validateReferenceImages can skip remote URLs for providers that support them", async () => {
|
||||
await validateReferenceImages(["https://example.com/ref.png"], { allowRemoteUrls: true });
|
||||
|
||||
await assert.rejects(
|
||||
() => validateReferenceImages(["https://example.com/ref.png"]),
|
||||
/Reference image not found/,
|
||||
);
|
||||
});
|
||||
|
||||
test("parseSimpleYaml parses nested defaults and provider limits", () => {
|
||||
const yaml = `
|
||||
version: 2
|
||||
@@ -308,7 +318,7 @@ test("detectProvider rejects non-ref-capable providers and prefers Google first
|
||||
() =>
|
||||
detectProvider(
|
||||
makeArgs({
|
||||
provider: "dashscope",
|
||||
provider: "zai",
|
||||
referenceImages: ["ref.png"],
|
||||
}),
|
||||
),
|
||||
@@ -426,6 +436,33 @@ test("detectProvider infers Seedream from model id and allows Seedream reference
|
||||
);
|
||||
});
|
||||
|
||||
test("detectProvider allows DashScope reference-image workflows when explicitly chosen for wan2.7 models", (t) => {
|
||||
useEnv(t, {
|
||||
GOOGLE_API_KEY: null,
|
||||
OPENAI_API_KEY: null,
|
||||
AZURE_OPENAI_API_KEY: null,
|
||||
AZURE_OPENAI_BASE_URL: null,
|
||||
OPENROUTER_API_KEY: null,
|
||||
DASHSCOPE_API_KEY: "dashscope-key",
|
||||
MINIMAX_API_KEY: null,
|
||||
REPLICATE_API_TOKEN: null,
|
||||
JIMENG_ACCESS_KEY_ID: null,
|
||||
JIMENG_SECRET_ACCESS_KEY: null,
|
||||
ARK_API_KEY: null,
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
detectProvider(
|
||||
makeArgs({
|
||||
provider: "dashscope",
|
||||
model: "wan2.7-image-pro",
|
||||
referenceImages: ["ref.png"],
|
||||
}),
|
||||
),
|
||||
"dashscope",
|
||||
);
|
||||
});
|
||||
|
||||
test("detectProvider selects MiniMax when only MiniMax credentials are configured or the model id matches", (t) => {
|
||||
useEnv(t, {
|
||||
GOOGLE_API_KEY: null,
|
||||
@@ -504,7 +541,7 @@ test("loadBatchTasks and createTaskArgs resolve batch-relative paths", async (t)
|
||||
id: "hero",
|
||||
promptFiles: ["prompts/hero.md"],
|
||||
image: "out/hero",
|
||||
ref: ["refs/hero.png"],
|
||||
ref: ["refs/hero.png", "https://example.com/ref.png"],
|
||||
ar: "16:9",
|
||||
},
|
||||
],
|
||||
@@ -533,6 +570,7 @@ test("loadBatchTasks and createTaskArgs resolve batch-relative paths", async (t)
|
||||
assert.equal(taskArgs.imagePath, path.join(loaded.batchDir, "out/hero"));
|
||||
assert.deepEqual(taskArgs.referenceImages, [
|
||||
path.join(loaded.batchDir, "refs/hero.png"),
|
||||
"https://example.com/ref.png",
|
||||
]);
|
||||
assert.equal(taskArgs.provider, "replicate");
|
||||
assert.equal(taskArgs.aspectRatio, "16:9");
|
||||
@@ -557,5 +595,29 @@ test("path normalization, worker count, and retry classification follow expected
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
isRetryableGenerationError(
|
||||
new Error("DashScope wan2.7 image models accept at most 9 reference images. Received 10."),
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
isRetryableGenerationError(
|
||||
new Error("DashScope wan2.7 image models in baoyu-imagine support exactly one output image per request."),
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
isRetryableGenerationError(
|
||||
new Error("DashScope wan2.7 image models support aspect ratios in [1:8, 8:1]."),
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
isRetryableGenerationError(
|
||||
new Error("DashScope wan2.7-image requires total pixels between 768*768 and 2048*2048."),
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.equal(isRetryableGenerationError(new Error("socket hang up")), true);
|
||||
});
|
||||
|
||||
@@ -85,7 +85,7 @@ Options:
|
||||
--quality normal|2k Quality preset (default: 2k)
|
||||
--imageSize 1K|2K|4K Image size for Google/OpenRouter (default: from quality)
|
||||
--imageApiDialect <id> OpenAI-compatible image dialect: openai-native|ratio-metadata
|
||||
--ref <files...> Reference images (Google, OpenAI, Azure, OpenRouter, Replicate supported families, MiniMax, or Seedream 4.0/4.5/5.0)
|
||||
--ref <files...> Reference images (Google, OpenAI, Azure, OpenRouter, Replicate supported families, MiniMax, Seedream 4.0/4.5/5.0, or DashScope wan2.7-image*)
|
||||
--n <count> Number of images for the current task (default: 1; Replicate currently requires 1)
|
||||
--json JSON output
|
||||
-h, --help Show help
|
||||
@@ -698,10 +698,11 @@ export function detectProvider(args: CliArgs): Provider {
|
||||
args.provider !== "openrouter" &&
|
||||
args.provider !== "replicate" &&
|
||||
args.provider !== "seedream" &&
|
||||
args.provider !== "minimax"
|
||||
args.provider !== "minimax" &&
|
||||
args.provider !== "dashscope"
|
||||
) {
|
||||
throw new Error(
|
||||
"Reference images require a ref-capable provider. Use --provider google (Gemini multimodal), --provider openai (GPT Image edits), --provider azure (Azure OpenAI), --provider openrouter (OpenRouter multimodal), --provider replicate, --provider seedream for supported Seedream models, or --provider minimax for MiniMax subject-reference workflows."
|
||||
"Reference images require a ref-capable provider. Use --provider google (Gemini multimodal), --provider openai (GPT Image edits), --provider azure (Azure OpenAI), --provider openrouter (OpenRouter multimodal), --provider replicate, --provider dashscope with a wan2.7 image model, --provider seedream for supported Seedream models, or --provider minimax for MiniMax subject-reference workflows."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -775,8 +776,24 @@ export function detectProvider(args: CliArgs): Provider {
|
||||
);
|
||||
}
|
||||
|
||||
export async function validateReferenceImages(referenceImages: string[]): Promise<void> {
|
||||
export type ReferenceImageValidationOptions = {
|
||||
allowRemoteUrls?: boolean;
|
||||
};
|
||||
|
||||
function isRemoteReferenceImage(refPath: string): boolean {
|
||||
return /^https?:\/\//i.test(refPath);
|
||||
}
|
||||
|
||||
function shouldAllowRemoteReferenceImages(provider: Provider | null): boolean {
|
||||
return provider === "dashscope";
|
||||
}
|
||||
|
||||
export async function validateReferenceImages(
|
||||
referenceImages: string[],
|
||||
options: ReferenceImageValidationOptions = {},
|
||||
): Promise<void> {
|
||||
for (const refPath of referenceImages) {
|
||||
if (options.allowRemoteUrls && isRemoteReferenceImage(refPath)) continue;
|
||||
const fullPath = path.resolve(refPath);
|
||||
try {
|
||||
await access(fullPath);
|
||||
@@ -803,6 +820,11 @@ export function isRetryableGenerationError(error: unknown): boolean {
|
||||
"API error (404)",
|
||||
"temporarily disabled",
|
||||
"supports saving exactly one image",
|
||||
"supports only",
|
||||
"support exactly one output image",
|
||||
"support aspect ratios in",
|
||||
"requires total pixels between",
|
||||
"accept at most",
|
||||
];
|
||||
return !nonRetryableMarkers.some((marker) => msg.includes(marker));
|
||||
}
|
||||
@@ -858,7 +880,11 @@ async function prepareSingleTask(args: CliArgs, extendConfig: Partial<ExtendConf
|
||||
const prompt = (await loadPromptForArgs(args)) ?? (await readPromptFromStdin());
|
||||
if (!prompt) throw new Error("Prompt is required");
|
||||
if (!args.imagePath) throw new Error("--image is required");
|
||||
if (args.referenceImages.length > 0) await validateReferenceImages(args.referenceImages);
|
||||
if (args.referenceImages.length > 0) {
|
||||
await validateReferenceImages(args.referenceImages, {
|
||||
allowRemoteUrls: shouldAllowRemoteReferenceImages(args.provider),
|
||||
});
|
||||
}
|
||||
|
||||
const provider = detectProvider(args);
|
||||
const providerModule = await loadProviderModule(provider);
|
||||
@@ -907,6 +933,10 @@ export function resolveBatchPath(batchDir: string, filePath: string): string {
|
||||
return path.isAbsolute(filePath) ? filePath : path.resolve(batchDir, filePath);
|
||||
}
|
||||
|
||||
function resolveBatchReferencePath(batchDir: string, filePath: string): string {
|
||||
return isRemoteReferenceImage(filePath) ? filePath : resolveBatchPath(batchDir, filePath);
|
||||
}
|
||||
|
||||
export function createTaskArgs(baseArgs: CliArgs, task: BatchTaskInput, batchDir: string): CliArgs {
|
||||
return {
|
||||
...baseArgs,
|
||||
@@ -922,7 +952,7 @@ export function createTaskArgs(baseArgs: CliArgs, task: BatchTaskInput, batchDir
|
||||
imageSize: task.imageSize ?? baseArgs.imageSize ?? null,
|
||||
imageSizeSource: task.imageSize != null ? "task" : (baseArgs.imageSizeSource ?? null),
|
||||
imageApiDialect: task.imageApiDialect ?? baseArgs.imageApiDialect ?? null,
|
||||
referenceImages: task.ref ? task.ref.map((filePath) => resolveBatchPath(batchDir, filePath)) : [],
|
||||
referenceImages: task.ref ? task.ref.map((filePath) => resolveBatchReferencePath(batchDir, filePath)) : [],
|
||||
n: task.n ?? baseArgs.n,
|
||||
batchFile: null,
|
||||
jobs: baseArgs.jobs,
|
||||
@@ -946,7 +976,11 @@ async function prepareBatchTasks(
|
||||
const prompt = await loadPromptForArgs(taskArgs);
|
||||
if (!prompt) throw new Error(`Task ${i + 1} is missing prompt or promptFiles.`);
|
||||
if (!taskArgs.imagePath) throw new Error(`Task ${i + 1} is missing image output path.`);
|
||||
if (taskArgs.referenceImages.length > 0) await validateReferenceImages(taskArgs.referenceImages);
|
||||
if (taskArgs.referenceImages.length > 0) {
|
||||
await validateReferenceImages(taskArgs.referenceImages, {
|
||||
allowRemoteUrls: shouldAllowRemoteReferenceImages(taskArgs.provider),
|
||||
});
|
||||
}
|
||||
|
||||
const provider = detectProvider(taskArgs);
|
||||
const providerModule = await loadProviderModule(provider);
|
||||
|
||||
@@ -2,15 +2,42 @@ import assert from "node:assert/strict";
|
||||
import test, { type TestContext } from "node:test";
|
||||
|
||||
import {
|
||||
generateImage,
|
||||
getDefaultModel,
|
||||
getModelFamily,
|
||||
getQwen2SizeFromAspectRatio,
|
||||
getSizeFromAspectRatio,
|
||||
getWan27SizeFromAspectRatio,
|
||||
normalizeSize,
|
||||
parseAspectRatio,
|
||||
parseSize,
|
||||
resolveSizeForModel,
|
||||
} from "./dashscope.ts";
|
||||
import type { CliArgs } from "../types.ts";
|
||||
|
||||
function makeCliArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
return {
|
||||
prompt: null,
|
||||
promptFiles: [],
|
||||
imagePath: null,
|
||||
provider: "dashscope",
|
||||
model: null,
|
||||
aspectRatio: null,
|
||||
aspectRatioSource: null,
|
||||
size: null,
|
||||
quality: "2k",
|
||||
imageSize: null,
|
||||
imageSizeSource: null,
|
||||
imageApiDialect: null,
|
||||
referenceImages: [],
|
||||
n: 1,
|
||||
batchFile: null,
|
||||
jobs: null,
|
||||
json: false,
|
||||
help: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function useEnv(
|
||||
t: TestContext,
|
||||
@@ -51,9 +78,11 @@ test("DashScope aspect-ratio parsing accepts numeric ratios only", () => {
|
||||
assert.equal(parseAspectRatio("-1:2"), null);
|
||||
});
|
||||
|
||||
test("DashScope model family routing distinguishes qwen-2.0, fixed-size qwen, and legacy models", () => {
|
||||
test("DashScope model family routing distinguishes qwen-2.0, fixed-size qwen, wan2.7, and legacy models", () => {
|
||||
assert.equal(getModelFamily("qwen-image-2.0-pro"), "qwen2");
|
||||
assert.equal(getModelFamily("qwen-image"), "qwenFixed");
|
||||
assert.equal(getModelFamily("wan2.7-image"), "wan27");
|
||||
assert.equal(getModelFamily("wan2.7-image-pro"), "wan27");
|
||||
assert.equal(getModelFamily("z-image-turbo"), "legacy");
|
||||
assert.equal(getModelFamily("wanx-v1"), "legacy");
|
||||
});
|
||||
@@ -146,3 +175,218 @@ test("DashScope size normalization converts WxH into provider format", () => {
|
||||
assert.equal(normalizeSize("1024x1024"), "1024*1024");
|
||||
assert.equal(normalizeSize("2048*1152"), "2048*1152");
|
||||
});
|
||||
|
||||
test("Wan 2.7 derives sizes that match the requested ratio at the chosen pixel budget", () => {
|
||||
const square2k = getWan27SizeFromAspectRatio(null, "2k", 2048 * 2048);
|
||||
const parsedSquare = parseSize(square2k);
|
||||
assert.ok(parsedSquare);
|
||||
assert.equal(parsedSquare.width, parsedSquare.height);
|
||||
assert.ok(parsedSquare.width * parsedSquare.height <= 2048 * 2048);
|
||||
|
||||
const widescreen = getWan27SizeFromAspectRatio("16:9", "2k", 2048 * 2048);
|
||||
const parsedWide = parseSize(widescreen);
|
||||
assert.ok(parsedWide);
|
||||
assert.ok(Math.abs(parsedWide.width / parsedWide.height - 16 / 9) < 0.05);
|
||||
assert.ok(parsedWide.width * parsedWide.height <= 2048 * 2048);
|
||||
|
||||
const pro4k = getWan27SizeFromAspectRatio("16:9", "2k", 4096 * 4096);
|
||||
const parsed4k = parseSize(pro4k);
|
||||
assert.ok(parsed4k);
|
||||
assert.ok(parsed4k.width * parsed4k.height > 2048 * 2048);
|
||||
assert.ok(parsed4k.width * parsed4k.height <= 4096 * 4096);
|
||||
});
|
||||
|
||||
test("Wan 2.7 rejects aspect ratios outside the [1:8, 8:1] range", () => {
|
||||
assert.throws(
|
||||
() => getWan27SizeFromAspectRatio("9:1", "2k", 2048 * 2048),
|
||||
/1:8, 8:1/,
|
||||
);
|
||||
assert.throws(
|
||||
() => getWan27SizeFromAspectRatio("1:9", "normal", 2048 * 2048),
|
||||
/1:8, 8:1/,
|
||||
);
|
||||
});
|
||||
|
||||
test("Wan 2.7 derived sizes stay inside the boundary ratio limits after rounding", () => {
|
||||
for (const ar of ["8:1", "1:8"]) {
|
||||
const size = getWan27SizeFromAspectRatio(ar, "2k", 2048 * 2048);
|
||||
const parsed = parseSize(size);
|
||||
assert.ok(parsed);
|
||||
const ratio = parsed.width / parsed.height;
|
||||
assert.ok(ratio >= 1 / 8);
|
||||
assert.ok(ratio <= 8);
|
||||
assert.ok(parsed.width * parsed.height <= 2048 * 2048);
|
||||
}
|
||||
});
|
||||
|
||||
test("resolveSizeForModel routes wan2.7-image to the 2K-capped derivation", () => {
|
||||
const size = resolveSizeForModel("wan2.7-image", {
|
||||
size: null,
|
||||
aspectRatio: "16:9",
|
||||
quality: "2k",
|
||||
});
|
||||
const parsed = parseSize(size);
|
||||
assert.ok(parsed);
|
||||
assert.ok(parsed.width * parsed.height <= 2048 * 2048);
|
||||
assert.ok(Math.abs(parsed.width / parsed.height - 16 / 9) < 0.05);
|
||||
});
|
||||
|
||||
test("resolveSizeForModel allows wan2.7-image-pro 4K only when there are no reference images", () => {
|
||||
assert.equal(
|
||||
resolveSizeForModel("wan2.7-image-pro", {
|
||||
size: "4096*4096",
|
||||
aspectRatio: null,
|
||||
quality: "2k",
|
||||
}),
|
||||
"4096*4096",
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
resolveSizeForModel("wan2.7-image-pro", {
|
||||
size: "4096*4096",
|
||||
aspectRatio: null,
|
||||
quality: "2k",
|
||||
referenceImages: ["a.png"],
|
||||
}),
|
||||
/total pixels between 768\*768 and 2048\*2048/,
|
||||
);
|
||||
|
||||
const proWithRef = resolveSizeForModel("wan2.7-image-pro", {
|
||||
size: null,
|
||||
aspectRatio: "1:1",
|
||||
quality: "2k",
|
||||
referenceImages: ["a.png"],
|
||||
});
|
||||
const parsedRef = parseSize(proWithRef);
|
||||
assert.ok(parsedRef);
|
||||
assert.ok(parsedRef.width * parsedRef.height <= 2048 * 2048);
|
||||
});
|
||||
|
||||
test("Wan 2.7 request body forces n=1 and omits prompt_extend / negative_prompt", async (t) => {
|
||||
useEnv(t, { DASHSCOPE_API_KEY: "fake-key" });
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
let capturedBody: any = null;
|
||||
globalThis.fetch = (async (_url: string, init?: RequestInit) => {
|
||||
capturedBody = JSON.parse(String(init?.body));
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
output: {
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content: [{ image: "data:image/png;base64,iVBORw0KGgo=" }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
);
|
||||
}) as typeof fetch;
|
||||
t.after(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
await generateImage("hello", "wan2.7-image-pro", makeCliArgs({ aspectRatio: "1:1" }));
|
||||
|
||||
assert.equal(capturedBody.model, "wan2.7-image-pro");
|
||||
assert.deepEqual(Object.keys(capturedBody.parameters).sort(), ["n", "size", "watermark"]);
|
||||
assert.equal(capturedBody.parameters.n, 1);
|
||||
assert.equal(capturedBody.parameters.watermark, false);
|
||||
assert.equal(typeof capturedBody.parameters.size, "string");
|
||||
assert.ok(!("prompt_extend" in capturedBody.parameters));
|
||||
assert.ok(!("negative_prompt" in capturedBody.parameters));
|
||||
|
||||
assert.deepEqual(capturedBody.input.messages[0].content, [{ text: "hello" }]);
|
||||
});
|
||||
|
||||
test("Wan 2.7 request body forwards remote reference image URLs", async (t) => {
|
||||
useEnv(t, { DASHSCOPE_API_KEY: "fake-key" });
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
let capturedBody: any = null;
|
||||
globalThis.fetch = (async (_url: string, init?: RequestInit) => {
|
||||
capturedBody = JSON.parse(String(init?.body));
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
output: {
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content: [{ image: "data:image/png;base64,iVBORw0KGgo=" }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
);
|
||||
}) as typeof fetch;
|
||||
t.after(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
await generateImage(
|
||||
"combine these",
|
||||
"wan2.7-image-pro",
|
||||
makeCliArgs({ referenceImages: ["https://example.com/ref.png"] }),
|
||||
);
|
||||
|
||||
assert.deepEqual(capturedBody.input.messages[0].content, [
|
||||
{ image: "https://example.com/ref.png" },
|
||||
{ text: "combine these" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("Wan 2.7 rejects --n > 1 to prevent silent multi-image billing", async (t) => {
|
||||
useEnv(t, { DASHSCOPE_API_KEY: "fake-key" });
|
||||
|
||||
await assert.rejects(
|
||||
() => generateImage("hi", "wan2.7-image-pro", makeCliArgs({ n: 2 })),
|
||||
/support exactly one output image/,
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveSizeForModel validates explicit wan2.7 sizes by pixel budget and ratio", () => {
|
||||
assert.equal(
|
||||
resolveSizeForModel("wan2.7-image-pro", {
|
||||
size: "3840x2160",
|
||||
aspectRatio: null,
|
||||
quality: "2k",
|
||||
}),
|
||||
"3840*2160",
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
resolveSizeForModel("wan2.7-image-pro", {
|
||||
size: "3840x2160",
|
||||
aspectRatio: null,
|
||||
quality: "2k",
|
||||
referenceImages: ["a.png"],
|
||||
}),
|
||||
/total pixels between 768\*768 and 2048\*2048/,
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
resolveSizeForModel("wan2.7-image", {
|
||||
size: "4096x4096",
|
||||
aspectRatio: null,
|
||||
quality: "2k",
|
||||
}),
|
||||
/total pixels between 768\*768 and 2048\*2048/,
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
resolveSizeForModel("wan2.7-image-pro", {
|
||||
size: "3072*256",
|
||||
aspectRatio: null,
|
||||
quality: "2k",
|
||||
}),
|
||||
/1:8, 8:1/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import path from "node:path";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import type { CliArgs, Quality } from "../types";
|
||||
|
||||
type DashScopeModelFamily = "qwen2" | "qwenFixed" | "legacy";
|
||||
type DashScopeModelFamily = "qwen2" | "qwenFixed" | "wan27" | "legacy";
|
||||
|
||||
type DashScopeModelSpec = {
|
||||
family: DashScopeModelFamily;
|
||||
@@ -19,6 +21,16 @@ const QWEN_2_TARGET_PIXELS: Record<Quality, number> = {
|
||||
"2k": 1536 * 1536,
|
||||
};
|
||||
|
||||
const MIN_WAN27_TOTAL_PIXELS = 768 * 768;
|
||||
const MAX_WAN27_PRO_T2I_PIXELS = 4096 * 4096;
|
||||
const MAX_WAN27_GENERAL_PIXELS = 2048 * 2048;
|
||||
const WAN27_MAX_REFERENCE_IMAGES = 9;
|
||||
|
||||
const WAN27_TARGET_PIXELS: Record<Quality, number> = {
|
||||
normal: 1024 * 1024,
|
||||
"2k": 2048 * 2048,
|
||||
};
|
||||
|
||||
const QWEN_2_RECOMMENDED: Record<string, Record<Quality, string>> = {
|
||||
"1:1": { normal: "1024*1024", "2k": "1536*1536" },
|
||||
"2:3": { normal: "768*1152", "2k": "1024*1536" },
|
||||
@@ -73,6 +85,11 @@ const QWEN_FIXED_SPEC: DashScopeModelSpec = {
|
||||
defaultSize: QWEN_FIXED_SIZES_BY_RATIO["16:9"],
|
||||
};
|
||||
|
||||
const WAN27_SPEC: DashScopeModelSpec = {
|
||||
family: "wan27",
|
||||
defaultSize: "2048*2048",
|
||||
};
|
||||
|
||||
const LEGACY_SPEC: DashScopeModelSpec = {
|
||||
family: "legacy",
|
||||
defaultSize: "1536*1536",
|
||||
@@ -88,12 +105,31 @@ const MODEL_SPEC_ALIASES: Record<string, DashScopeModelSpec> = {
|
||||
"qwen-image-plus": QWEN_FIXED_SPEC,
|
||||
"qwen-image-plus-2026-01-09": QWEN_FIXED_SPEC,
|
||||
"qwen-image": QWEN_FIXED_SPEC,
|
||||
"wan2.7-image-pro": WAN27_SPEC,
|
||||
"wan2.7-image": WAN27_SPEC,
|
||||
};
|
||||
|
||||
export function getDefaultModel(): string {
|
||||
return process.env.DASHSCOPE_IMAGE_MODEL || DEFAULT_MODEL;
|
||||
}
|
||||
|
||||
function getReferenceImageMime(filePath: string): string {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
|
||||
if (ext === ".webp") return "image/webp";
|
||||
if (ext === ".bmp") return "image/bmp";
|
||||
return "image/png";
|
||||
}
|
||||
|
||||
async function loadReferenceImage(refPath: string): Promise<string> {
|
||||
if (/^https?:\/\//i.test(refPath)) {
|
||||
return refPath;
|
||||
}
|
||||
const fullPath = path.resolve(refPath);
|
||||
const bytes = await readFile(fullPath);
|
||||
return `data:${getReferenceImageMime(fullPath)};base64,${bytes.toString("base64")}`;
|
||||
}
|
||||
|
||||
function getApiKey(): string | null {
|
||||
return process.env.DASHSCOPE_API_KEY || null;
|
||||
}
|
||||
@@ -173,6 +209,10 @@ function roundToStep(value: number): number {
|
||||
return Math.max(SIZE_STEP, Math.round(value / SIZE_STEP) * SIZE_STEP);
|
||||
}
|
||||
|
||||
function floorToStep(value: number): number {
|
||||
return Math.max(SIZE_STEP, Math.floor(value / SIZE_STEP) * SIZE_STEP);
|
||||
}
|
||||
|
||||
function fitToPixelBudget(
|
||||
width: number,
|
||||
height: number,
|
||||
@@ -220,6 +260,21 @@ function fitToPixelBudget(
|
||||
return { width: roundedWidth, height: roundedHeight };
|
||||
}
|
||||
|
||||
function clampWan27DerivedSizeToRatioBounds(
|
||||
size: { width: number; height: number },
|
||||
): { width: number; height: number } {
|
||||
let { width, height } = size;
|
||||
const ratio = width / height;
|
||||
|
||||
if (ratio > 8) {
|
||||
width = floorToStep(height * 8);
|
||||
} else if (ratio < 1 / 8) {
|
||||
height = floorToStep(width * 8);
|
||||
}
|
||||
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
export function getSizeFromAspectRatio(ar: string | null, quality: CliArgs["quality"]): string {
|
||||
const normalizedQuality = normalizeQuality(quality);
|
||||
const sizes = normalizedQuality === "2k" ? LEGACY_STANDARD_SIZES_2K : LEGACY_STANDARD_SIZES;
|
||||
@@ -276,6 +331,77 @@ export function getQwen2SizeFromAspectRatio(ar: string | null, quality: CliArgs[
|
||||
return formatSize(fitted.width, fitted.height);
|
||||
}
|
||||
|
||||
function isWan27ProModel(model: string): boolean {
|
||||
return model.trim().toLowerCase() === "wan2.7-image-pro";
|
||||
}
|
||||
|
||||
function getWan27MaxPixels(model: string, hasReferenceImages: boolean): number {
|
||||
if (isWan27ProModel(model) && !hasReferenceImages) {
|
||||
return MAX_WAN27_PRO_T2I_PIXELS;
|
||||
}
|
||||
return MAX_WAN27_GENERAL_PIXELS;
|
||||
}
|
||||
|
||||
export function getWan27SizeFromAspectRatio(
|
||||
ar: string | null,
|
||||
quality: CliArgs["quality"],
|
||||
maxPixels: number,
|
||||
): string {
|
||||
const normalizedQuality = normalizeQuality(quality);
|
||||
const targetPixels = Math.min(WAN27_TARGET_PIXELS[normalizedQuality], maxPixels);
|
||||
|
||||
if (!ar) {
|
||||
const side = roundToStep(Math.sqrt(targetPixels));
|
||||
return formatSize(side, side);
|
||||
}
|
||||
|
||||
const parsed = parseAspectRatio(ar);
|
||||
if (!parsed) {
|
||||
const side = roundToStep(Math.sqrt(targetPixels));
|
||||
return formatSize(side, side);
|
||||
}
|
||||
|
||||
const ratio = parsed.width / parsed.height;
|
||||
if (ratio < 1 / 8 || ratio > 8) {
|
||||
throw new Error(
|
||||
`DashScope wan2.7 image models support aspect ratios in [1:8, 8:1]. Received "${ar}".`
|
||||
);
|
||||
}
|
||||
|
||||
const rawWidth = Math.sqrt(targetPixels * ratio);
|
||||
const rawHeight = Math.sqrt(targetPixels / ratio);
|
||||
const fitted = fitToPixelBudget(
|
||||
rawWidth,
|
||||
rawHeight,
|
||||
MIN_WAN27_TOTAL_PIXELS,
|
||||
maxPixels,
|
||||
);
|
||||
const bounded = clampWan27DerivedSizeToRatioBounds(fitted);
|
||||
|
||||
return formatSize(bounded.width, bounded.height);
|
||||
}
|
||||
|
||||
function validateWan27Size(size: string, maxPixels: number, model: string): string {
|
||||
const normalized = normalizeSize(size);
|
||||
const parsed = validateSizeFormat(normalized);
|
||||
const totalPixels = parsed.width * parsed.height;
|
||||
if (totalPixels < MIN_WAN27_TOTAL_PIXELS || totalPixels > maxPixels) {
|
||||
const limit = maxPixels === MAX_WAN27_PRO_T2I_PIXELS ? "4096*4096" : "2048*2048";
|
||||
throw new Error(
|
||||
`DashScope ${model} requires total pixels between 768*768 and ${limit} ` +
|
||||
`for the current request. Received ${normalized} (${totalPixels} pixels).`
|
||||
);
|
||||
}
|
||||
const ratio = parsed.width / parsed.height;
|
||||
if (ratio < 1 / 8 || ratio > 8) {
|
||||
throw new Error(
|
||||
`DashScope wan2.7 image models support aspect ratios in [1:8, 8:1]. ` +
|
||||
`Received ${normalized} (ratio ${ratio.toFixed(3)}).`
|
||||
);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function getQwenFixedSizeFromAspectRatio(ar: string | null, quality: CliArgs["quality"]): string {
|
||||
if (quality === "normal") {
|
||||
console.warn(
|
||||
@@ -331,9 +457,16 @@ function validateQwenFixedSize(size: string): string {
|
||||
|
||||
export function resolveSizeForModel(
|
||||
model: string,
|
||||
args: Pick<CliArgs, "size" | "aspectRatio" | "quality">,
|
||||
args: Pick<CliArgs, "size" | "aspectRatio" | "quality"> & { referenceImages?: string[] },
|
||||
): string {
|
||||
const spec = getModelSpec(model);
|
||||
const referenceCount = args.referenceImages?.length ?? 0;
|
||||
|
||||
if (spec.family === "wan27") {
|
||||
const maxPixels = getWan27MaxPixels(model, referenceCount > 0);
|
||||
if (args.size) return validateWan27Size(args.size, maxPixels, model);
|
||||
return getWan27SizeFromAspectRatio(args.aspectRatio, args.quality, maxPixels);
|
||||
}
|
||||
|
||||
if (args.size) {
|
||||
if (spec.family === "qwen2") return validateQwen2Size(args.size);
|
||||
@@ -357,6 +490,14 @@ function buildParameters(
|
||||
family: DashScopeModelFamily,
|
||||
size: string,
|
||||
): Record<string, unknown> {
|
||||
if (family === "wan27") {
|
||||
return {
|
||||
size,
|
||||
n: 1,
|
||||
watermark: false,
|
||||
};
|
||||
}
|
||||
|
||||
const parameters: Record<string, unknown> = {
|
||||
prompt_extend: false,
|
||||
size,
|
||||
@@ -419,23 +560,44 @@ export async function generateImage(
|
||||
const apiKey = getApiKey();
|
||||
if (!apiKey) throw new Error("DASHSCOPE_API_KEY is required");
|
||||
|
||||
if (args.referenceImages.length > 0) {
|
||||
const spec = getModelSpec(model);
|
||||
|
||||
if (args.referenceImages.length > 0 && spec.family !== "wan27") {
|
||||
throw new Error(
|
||||
"Reference images are not supported with DashScope provider in baoyu-imagine. Use --provider google with a Gemini multimodal model."
|
||||
"Reference images are not supported with this DashScope model. Use a wan2.7 image model (--model wan2.7-image-pro or wan2.7-image), or switch to --provider google with a Gemini multimodal model."
|
||||
);
|
||||
}
|
||||
|
||||
if (args.referenceImages.length > WAN27_MAX_REFERENCE_IMAGES) {
|
||||
throw new Error(
|
||||
`DashScope wan2.7 image models accept at most ${WAN27_MAX_REFERENCE_IMAGES} reference images. Received ${args.referenceImages.length}.`
|
||||
);
|
||||
}
|
||||
|
||||
if (spec.family === "wan27" && args.n !== 1) {
|
||||
throw new Error(
|
||||
"DashScope wan2.7 image models in baoyu-imagine support exactly one output image per request (extra images would be billed but discarded). Remove --n or use --n 1."
|
||||
);
|
||||
}
|
||||
|
||||
const spec = getModelSpec(model);
|
||||
const size = resolveSizeForModel(model, args);
|
||||
const url = `${getBaseUrl()}/api/v1/services/aigc/multimodal-generation/generation`;
|
||||
|
||||
const content: Array<Record<string, unknown>> = [];
|
||||
if (spec.family === "wan27" && args.referenceImages.length > 0) {
|
||||
for (const refPath of args.referenceImages) {
|
||||
content.push({ image: await loadReferenceImage(refPath) });
|
||||
}
|
||||
}
|
||||
content.push({ text: prompt });
|
||||
|
||||
const body = {
|
||||
model,
|
||||
input: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ text: prompt }],
|
||||
content,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: baoyu-infographic
|
||||
description: Generate professional infographics with 21 layout types and 21 visual styles. Analyzes content, recommends layout×style combinations, and generates publication-ready infographics. Use when user asks to create "infographic", "信息图", "visual summary", "可视化", or "高密度信息大图".
|
||||
version: 1.57.1
|
||||
description: Generate professional infographics with 21 layout types and 22 visual styles. Analyzes content, recommends layout×style combinations, and generates publication-ready infographics. Use when user asks to create "infographic", "信息图", "visual summary", "可视化", or "高密度信息大图".
|
||||
version: 1.58.0
|
||||
metadata:
|
||||
openclaw:
|
||||
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-infographic
|
||||
@@ -84,7 +84,7 @@ Default behavior: **confirm before generation**.
|
||||
| Option | Values |
|
||||
|--------|--------|
|
||||
| `--layout` | 21 options (see Layout Gallery), default: bento-grid |
|
||||
| `--style` | 21 options (see Style Gallery), default: craft-handmade |
|
||||
| `--style` | 22 options (see Style Gallery), default: craft-handmade |
|
||||
| `--aspect` | Named: landscape (16:9), portrait (9:16), square (1:1). Custom: any W:H ratio (e.g., 3:4, 4:3, 2.35:1) |
|
||||
| `--lang` | en, zh, ja, etc. |
|
||||
| `--no-confirm` | Skip Step 4 only when the user explicitly requests direct generation without confirmation |
|
||||
@@ -118,7 +118,7 @@ Default behavior: **confirm before generation**.
|
||||
|
||||
Full definitions live at `references/layouts/<layout>.md`.
|
||||
|
||||
## Style Gallery (21)
|
||||
## Style Gallery (22)
|
||||
|
||||
| Style | Description |
|
||||
|-------|-------------|
|
||||
@@ -143,6 +143,7 @@ Full definitions live at `references/layouts/<layout>.md`.
|
||||
| `morandi-journal` | Hand-drawn doodle, warm Morandi tones |
|
||||
| `retro-pop-grid` | 1970s retro pop art, Swiss grid, thick outlines |
|
||||
| `hand-drawn-edu` | Macaron pastels, hand-drawn wobble, stick figures |
|
||||
| `retro-popup-pop` | Retro popup collage, vintage UI, thick outlines, flat pop colors |
|
||||
|
||||
Full definitions live at `references/styles/<style>.md`.
|
||||
|
||||
@@ -165,6 +166,7 @@ Full definitions live at `references/styles/<style>.md`.
|
||||
| Product Guide | `dense-modules` + `morandi-journal` |
|
||||
| Technical Guide | `dense-modules` + `pop-laboratory` |
|
||||
| Trendy Guide | `dense-modules` + `retro-pop-grid` |
|
||||
| Retro Pop Guide | `dense-modules` + `retro-popup-pop` |
|
||||
| Educational Diagram | `hub-spoke` + `hand-drawn-edu` |
|
||||
| Process Tutorial | `linear-progression` + `hand-drawn-edu` |
|
||||
|
||||
@@ -176,7 +178,7 @@ When the user's input contains these keywords, use the mapped layout as the lead
|
||||
|
||||
| User Keyword | Layout | Recommended Styles | Default Aspect | Prompt Notes |
|
||||
|--------------|--------|--------------------|----------------|--------------|
|
||||
| 高密度信息大图 / high-density-info | `dense-modules` | `morandi-journal`, `pop-laboratory`, `retro-pop-grid` | portrait | — |
|
||||
| 高密度信息大图 / high-density-info | `dense-modules` | `morandi-journal`, `pop-laboratory`, `retro-pop-grid`, `retro-popup-pop` | portrait | — |
|
||||
| 信息图 / infographic | `bento-grid` | `craft-handmade` | landscape | Minimalist: clean canvas, ample whitespace, no complex background textures. Simple cartoon elements and icons only. |
|
||||
|
||||
## Output Structure
|
||||
|
||||
@@ -39,7 +39,7 @@ Approach content analysis as a **world-class instructional designer**:
|
||||
| **System/Structure** | Components, architecture, anatomy | structural-breakdown, bento-grid | technical-schematic, ikea-manual |
|
||||
| **Journey/Narrative** | Stories, user flows, milestones | winding-roadmap, story-mountain | storybook-watercolor, comic-strip |
|
||||
| **Overview/Summary** | Multiple topics, feature highlights | bento-grid, periodic-table, dense-modules | chalkboard, bold-graphic |
|
||||
| **Product/Buying Guide** | Multi-dimension comparisons, specs, pitfalls | dense-modules | morandi-journal, pop-laboratory, retro-pop-grid |
|
||||
| **Product/Buying Guide** | Multi-dimension comparisons, specs, pitfalls | dense-modules | morandi-journal, pop-laboratory, retro-pop-grid, retro-popup-pop |
|
||||
|
||||
### 2. Learning Objective Identification
|
||||
|
||||
|
||||
@@ -68,5 +68,6 @@ High-density modular layout with 6-7 typed information modules packed with concr
|
||||
- `pop-laboratory`: Technical precision with coordinate markers and blueprint grid
|
||||
- `morandi-journal`: Hand-drawn warmth with doodle illustrations and organic frames
|
||||
- `retro-pop-grid`: 1970s pop art with strict grid cells and bold contrast
|
||||
- `retro-popup-pop`: Vintage desktop popups with chunky pixel UI for retro-tech dense guides
|
||||
- `corporate-memphis`: Clean business feel for product comparisons
|
||||
- `technical-schematic`: Engineering precision for technical product guides
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# retro-popup-pop
|
||||
|
||||
Retro pixel popup × pop-art collage — content rendered as a stack of 80/90s desktop dialog windows with thick black outlines, flat color fills, and bright cyan or vintage cream backgrounds.
|
||||
|
||||
## Color Palette
|
||||
|
||||
- Background: Bright cyan (#12B8DE) primary canvas, vintage cream (#F5F0E6) alternate
|
||||
- Window fills: Vintage cream (#F5F0E6) and pure white (#FFFFFF)
|
||||
- Primary text and outlines: Pure black (#000000)
|
||||
- Reverse text: Pure white on solid black title bars
|
||||
- Accent fills (small areas only): Salmon pink, sky blue, mustard yellow, mint green — muted retro tones
|
||||
|
||||
## Visual Elements
|
||||
|
||||
- 80s/90s desktop popup windows with title bars, close buttons (×), and chunky borders
|
||||
- Multiple windows tiled or lightly overlapping with offset solid-black drop shadows for depth
|
||||
- ERROR / ALERT / WARNING dialogs to highlight misconceptions, common pitfalls, risks
|
||||
- File-window vignettes labeled with retro filenames (PROBLEMS.EXE, METHOD.PNG, BURNOUT.PSD, FOCUS_SCAN, SYSTEM_ALERT, IDEAS_MISSING)
|
||||
- Progress bars for completion, difficulty, conversion rate, or trend
|
||||
- Pixel-style chunky icons: folders, magnifiers, gears, exclamation marks, floppy disks, hourglasses
|
||||
- Action buttons with thick black borders and short pop-style copy: OK, CANCEL, FIX IT, SCAN, PLAY, RETRY, SAVE
|
||||
- Comparison data rendered as windowed lists or small tabular dialogs
|
||||
- Uniform thick black outlines on every window, button, icon, and divider
|
||||
- Pure 2D flat color fills throughout — no gradients, no glow, no glass
|
||||
|
||||
## Typography
|
||||
|
||||
- Headers: Pixel / dot-matrix / chunky bitmap display fonts, large and high-contrast
|
||||
- Body: Retro monospace or system-style sans-serif, high legibility
|
||||
- Title bars: Reverse white on solid black, all-caps preferred
|
||||
- Decorative all-caps English allowed for filenames, status strings, button labels (PROBLEMS.EXE, OK, CANCEL); body content text remains in the confirmed output language
|
||||
|
||||
## Style Enforcement
|
||||
|
||||
- Every information chunk must live inside a window, dialog, button, or progress bar — no floating elements
|
||||
- Uniform thick black outlines everywhere; offset solid-black drop shadows for stacked-window depth
|
||||
- Limit accent palette to ~4 colors per composition; let cyan or cream dominate the canvas
|
||||
- Maintain low-fi pixel/popup feel; humorous popup copy welcome, polished modern UI prohibited
|
||||
|
||||
## Avoid
|
||||
|
||||
- Modern UI styles: glassmorphism, neumorphism, soft shadows, blurs, gradients
|
||||
- 3D rendering, photorealism, ray-traced shadows
|
||||
- Hand-drawn wobble, watercolor washes, or sketch textures
|
||||
- Smooth anti-aliased curves on icons (prefer chunky pixel/stair-step edges)
|
||||
- Pure black backgrounds — keep cyan or cream as the canvas
|
||||
|
||||
## Best For
|
||||
|
||||
High-density "干货" knowledge guides, common-pitfall and debunking posts, knowledge-pop content for design-savvy or developer audiences, Xiaohongshu-style retro-tech posts, dense-modules portrait infographics.
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: baoyu-post-to-x
|
||||
description: Posts 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. Use when user asks to "post to X", "tweet", "publish to Twitter", or "share on X".
|
||||
version: 1.56.1
|
||||
description: Posts content and articles to X (Twitter). Supports regular posts with images/videos and X Articles (long-form Markdown). In Codex, prefers the bundled Chrome Computer Use when available and falls back to real Chrome CDP scripts otherwise. Use when user asks to "post to X", "tweet", "publish to Twitter", or "share on X".
|
||||
version: 1.57.0
|
||||
metadata:
|
||||
openclaw:
|
||||
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-post-to-x
|
||||
@@ -15,6 +15,8 @@ metadata:
|
||||
|
||||
Posts text, images, videos, and long-form articles to X via real Chrome browser (bypasses anti-bot detection).
|
||||
|
||||
In Codex, first try the bundled **Chrome Computer Use** path. Use the CDP scripts only when Chrome Computer Use is not available or the user explicitly asks for the script/CDP workflow.
|
||||
|
||||
## Script Directory
|
||||
|
||||
**Important**: All scripts are located in the `scripts/` subdirectory of this skill.
|
||||
@@ -28,15 +30,27 @@ Posts text, images, videos, and long-form articles to X via real Chrome browser
|
||||
**Script Reference**:
|
||||
| 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/x-browser.ts` | Regular posts (text + images), CDP fallback |
|
||||
| `scripts/x-video.ts` | Video posts (text + video), CDP fallback |
|
||||
| `scripts/x-quote.ts` | Quote tweet with comment, CDP fallback |
|
||||
| `scripts/x-article.ts` | Long-form article publishing (Markdown), CDP fallback |
|
||||
| `scripts/md-to-html.ts` | Markdown → HTML conversion |
|
||||
| `scripts/copy-to-clipboard.ts` | Copy content to clipboard |
|
||||
| `scripts/paste-from-clipboard.ts` | Send real paste keystroke |
|
||||
| `scripts/check-paste-permissions.ts` | Verify environment & permissions |
|
||||
|
||||
## Execution Mode Selection (Required)
|
||||
|
||||
Before choosing a workflow, detect whether Codex Chrome Computer Use is enabled:
|
||||
|
||||
1. If Computer Use tools are already visible, call `get_app_state` for app `Google Chrome`. In Codex these may be surfaced as `mcp__computer_use__.*`; use the exact available tool names.
|
||||
2. If they are not visible and `tool_search` is available, search for `computer-use get_app_state click press_key drag scroll Google Chrome`, then call `get_app_state` for app `Google Chrome`.
|
||||
3. If `get_app_state` succeeds, use **Chrome Computer Use Mode** below.
|
||||
4. If Computer Use tools are unavailable or `get_app_state` fails, use **CDP Script Mode**.
|
||||
5. If the user explicitly says to use Chrome Computer Use, do not fall back to CDP, Playwright, or the in-app Browser without telling the user and getting approval.
|
||||
|
||||
When Chrome Computer Use Mode is active, all X UI actions must go through the Codex Computer Use tools against the user's real Google Chrome. Shell commands are still allowed for preprocessing Markdown and copying local files to the system clipboard.
|
||||
|
||||
## Preferences (EXTEND.md)
|
||||
|
||||
Check EXTEND.md in priority order — the first one found wins:
|
||||
@@ -86,6 +100,73 @@ Checks: Chrome, profile isolation, Bun, Accessibility, clipboard, paste keystrok
|
||||
|
||||
---
|
||||
|
||||
## Chrome Computer Use Mode (Codex Preferred)
|
||||
|
||||
Use this mode whenever Codex can control `Google Chrome` with Computer Use. This uses the user's existing Chrome window, cookies, login, extensions, and X session.
|
||||
|
||||
**General rules**:
|
||||
- Start each assistant turn that controls Chrome by calling `get_app_state` for `Google Chrome`.
|
||||
- Prefer element-index actions when available; use coordinates only for editor text selection or drag selection.
|
||||
- Do not use the in-app Browser, Playwright, or CDP for X UI actions in this mode.
|
||||
- Never click `Publish`, `Post`, or any externally visible submit action without an explicit final confirmation from the user in the current conversation.
|
||||
|
||||
**Regular posts**:
|
||||
1. Open or navigate Chrome to `https://x.com/compose/post`.
|
||||
2. Type the post text into the composer using Computer Use.
|
||||
3. For each image, run:
|
||||
```bash
|
||||
${BUN_X} {baseDir}/scripts/copy-to-clipboard.ts image /absolute/path/to/image.png
|
||||
```
|
||||
4. Paste with Computer Use (`super+v` on macOS, `control+v` on Windows/Linux), then wait until X finishes uploading media.
|
||||
5. Ask for confirmation before clicking `Post`.
|
||||
|
||||
**Video posts**:
|
||||
1. Open or navigate Chrome to `https://x.com/compose/post`.
|
||||
2. Type the post text into the composer.
|
||||
3. Use the visible media upload/file picker UI to attach the video.
|
||||
4. Wait for upload and processing to complete.
|
||||
5. Ask for confirmation before clicking `Post`.
|
||||
|
||||
**Quote tweets**:
|
||||
1. Open the tweet URL in Chrome.
|
||||
2. Use the visible quote/repost UI to choose Quote.
|
||||
3. Type the comment.
|
||||
4. Ask for confirmation before clicking `Post`.
|
||||
|
||||
**X Articles**:
|
||||
1. Convert Markdown and keep the image map:
|
||||
```bash
|
||||
${BUN_X} {baseDir}/scripts/md-to-html.ts article.md --save-html /tmp/x-article-body.html > /tmp/x-article.json
|
||||
```
|
||||
2. Read the JSON output for `title`, `coverImage`, and `contentImages` (`placeholder` → `localPath`).
|
||||
3. In Chrome, open `https://x.com/compose/articles`, create or open the draft, upload the cover if present, and fill the title.
|
||||
4. Copy rich HTML to the clipboard:
|
||||
```bash
|
||||
${BUN_X} {baseDir}/scripts/copy-to-clipboard.ts html --file /tmp/x-article-body.html
|
||||
```
|
||||
5. Paste into the article body with Computer Use.
|
||||
6. For each `contentImages` entry in placeholder order:
|
||||
- Copy the image with `copy-to-clipboard.ts image <localPath>`.
|
||||
- Select the exact visible placeholder text such as `XIMGPH_3`.
|
||||
- Paste with Computer Use (`super+v`/`control+v`).
|
||||
- Wait until the X header no longer says `Uploading media...`.
|
||||
- If the placeholder remains, reselect the exact placeholder text and press `BackSpace`. Never press `BackSpace` unless the app state confirms the selected text is exactly that placeholder.
|
||||
7. Verify no `XIMGPH_` placeholders remain and the expected images appear.
|
||||
8. Open Preview and verify title, cover, body, links, and images.
|
||||
9. Ask for explicit confirmation before clicking `Publish`.
|
||||
|
||||
If Computer Use selection or paste becomes unreliable, stop and report the blocker instead of switching to CDP silently.
|
||||
|
||||
---
|
||||
|
||||
## CDP Script Mode (Fallback)
|
||||
|
||||
Use the script sections below only when Chrome Computer Use is unavailable, unreliable, or explicitly not requested. These scripts launch or reuse a real Chrome instance via CDP and keep the browser open for review.
|
||||
|
||||
Do not use CDP Script Mode when the user explicitly requires Chrome Computer Use unless the user approves the fallback after you explain the blocker.
|
||||
|
||||
---
|
||||
|
||||
## Post Type Selection
|
||||
|
||||
Unless the user explicitly specifies the post type:
|
||||
@@ -107,6 +188,8 @@ ${BUN_X} {baseDir}/scripts/x-browser.ts "Hello!" --image ./photo.png
|
||||
|
||||
**Note**: Script opens browser with content filled in. User reviews and publishes manually.
|
||||
|
||||
**Chrome Computer Use preferred**: In Codex, if Chrome Computer Use is enabled, use the visible Chrome UI workflow in **Chrome Computer Use Mode** instead of running `x-browser.ts`.
|
||||
|
||||
---
|
||||
|
||||
## Video Posts
|
||||
@@ -126,6 +209,8 @@ ${BUN_X} {baseDir}/scripts/x-video.ts "Check this out!" --video ./clip.mp4
|
||||
|
||||
**Note**: Script opens browser with content filled in. User reviews and publishes manually.
|
||||
|
||||
**Chrome Computer Use preferred**: In Codex, if Chrome Computer Use is enabled, use the visible Chrome UI workflow in **Chrome Computer Use Mode** instead of running `x-video.ts`.
|
||||
|
||||
**Limits**: Regular 140s max, Premium 60min. Processing: 30-60s.
|
||||
|
||||
---
|
||||
@@ -147,6 +232,8 @@ ${BUN_X} {baseDir}/scripts/x-quote.ts https://x.com/user/status/123 "Great insig
|
||||
|
||||
**Note**: Script opens browser with content filled in. User reviews and publishes manually.
|
||||
|
||||
**Chrome Computer Use preferred**: In Codex, if Chrome Computer Use is enabled, use the visible Chrome UI workflow in **Chrome Computer Use Mode** instead of running `x-quote.ts`.
|
||||
|
||||
---
|
||||
|
||||
## X Articles
|
||||
@@ -167,7 +254,11 @@ ${BUN_X} {baseDir}/scripts/x-article.ts article.md --cover ./cover.jpg
|
||||
|
||||
**Frontmatter**: `title`, `cover_image` supported in YAML front matter.
|
||||
|
||||
**Note**: Script opens browser with article filled in. User reviews and publishes manually.
|
||||
**Chrome Computer Use preferred**: In Codex, if Chrome Computer Use is enabled, follow **Chrome Computer Use Mode** above instead of running `x-article.ts`.
|
||||
|
||||
**CDP fallback note**: The script opens browser with article filled in. User reviews and publishes manually unless `--submit` is used.
|
||||
|
||||
**Publish safety**: Do not use `--submit` or click `Publish` unless the user explicitly confirms the final public publish action.
|
||||
|
||||
**Post-Composition Check**: The script automatically verifies after all images are inserted:
|
||||
- Remaining `XIMGPH_` placeholders in editor content
|
||||
@@ -181,7 +272,7 @@ If the check fails (warnings in output), alert the user with the specific issues
|
||||
|
||||
### Chrome debug port not ready
|
||||
|
||||
If a script fails with `Chrome debug port not ready` or `Unable to connect`, kill existing Chrome CDP instances first, then retry:
|
||||
CDP fallback only: if a script fails with `Chrome debug port not ready` or `Unable to connect`, kill existing Chrome CDP instances first, then retry:
|
||||
|
||||
```bash
|
||||
pkill -f "Chrome.*remote-debugging-port" 2>/dev/null; pkill -f "Chromium.*remote-debugging-port" 2>/dev/null; sleep 2
|
||||
@@ -192,7 +283,8 @@ pkill -f "Chrome.*remote-debugging-port" 2>/dev/null; pkill -f "Chromium.*remote
|
||||
## Notes
|
||||
|
||||
- First run: manual login required (session persists)
|
||||
- All scripts only fill content into the browser, user must review and publish manually
|
||||
- In Chrome Computer Use Mode, use the user's existing Chrome session and do not launch a separate CDP profile
|
||||
- CDP scripts only fill content into the browser by default; user must review and publish manually unless `--submit` is explicitly used
|
||||
- Cross-platform: macOS, Linux, Windows
|
||||
|
||||
## Extension Support
|
||||
|
||||
@@ -2,6 +2,17 @@
|
||||
|
||||
Publish Markdown articles to X Articles editor with rich text formatting and images.
|
||||
|
||||
## Mode Selection
|
||||
|
||||
In Codex, prefer **Chrome Computer Use** when available:
|
||||
|
||||
1. If Computer Use tools are already visible, call `get_app_state` for `Google Chrome`.
|
||||
2. If not, use `tool_search` for `computer-use get_app_state click press_key drag scroll Google Chrome`, then call `get_app_state`.
|
||||
3. If that succeeds, use the Computer Use workflow below.
|
||||
4. Use the CDP script workflow only when Computer Use is unavailable or explicitly requested.
|
||||
|
||||
If the user explicitly asks for Chrome Computer Use, do not fall back to CDP, Playwright, or the in-app Browser without approval.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- X Premium subscription (required for Articles)
|
||||
@@ -10,6 +21,24 @@ Publish Markdown articles to X Articles editor with rich text formatting and ima
|
||||
|
||||
## Usage
|
||||
|
||||
### Chrome Computer Use (Codex Preferred)
|
||||
|
||||
Prepare the article HTML and image map:
|
||||
|
||||
```bash
|
||||
${BUN_X} {baseDir}/scripts/md-to-html.ts article.md --save-html /tmp/x-article-body.html > /tmp/x-article.json
|
||||
```
|
||||
|
||||
Copy the generated HTML as rich text:
|
||||
|
||||
```bash
|
||||
${BUN_X} {baseDir}/scripts/copy-to-clipboard.ts html --file /tmp/x-article-body.html
|
||||
```
|
||||
|
||||
Then use Codex Computer Use against `Google Chrome` for all X UI operations.
|
||||
|
||||
### CDP Script Fallback
|
||||
|
||||
```bash
|
||||
# Publish markdown article (preview mode)
|
||||
${BUN_X} {baseDir}/scripts/x-article.ts article.md
|
||||
@@ -21,6 +50,8 @@ ${BUN_X} {baseDir}/scripts/x-article.ts article.md --cover ./cover.jpg
|
||||
${BUN_X} {baseDir}/scripts/x-article.ts article.md --submit
|
||||
```
|
||||
|
||||
Do not use `--submit` unless the user has explicitly confirmed the final public publish action.
|
||||
|
||||
## Markdown Format
|
||||
|
||||
```markdown
|
||||
@@ -118,7 +149,33 @@ JSON output:
|
||||
| `1. item` | `<ol><li>` |
|
||||
| `` | Image placeholder |
|
||||
|
||||
## Workflow
|
||||
## Computer Use Workflow (Preferred in Codex)
|
||||
|
||||
1. **Detect Computer Use**: call `get_app_state` for `Google Chrome`; use `tool_search` first if the tools are not visible.
|
||||
2. **Parse Markdown**: run `md-to-html.ts --save-html /tmp/x-article-body.html > /tmp/x-article.json`.
|
||||
3. **Read the map**: use `/tmp/x-article.json` for `title`, `coverImage`, and `contentImages`.
|
||||
4. **Open X Articles**: use Chrome Computer Use to navigate to `https://x.com/compose/articles`.
|
||||
5. **Create Draft**: click the create/write button if needed, or open the target draft.
|
||||
6. **Upload Cover**: if `coverImage` exists, use Chrome's visible upload/file picker UI. If the file picker cannot be operated reliably, stop and ask for help rather than switching to CDP silently.
|
||||
7. **Fill Title**: type the title into the title field.
|
||||
8. **Paste Content**:
|
||||
- Run `copy-to-clipboard.ts html --file /tmp/x-article-body.html`.
|
||||
- Click the article body.
|
||||
- Press `super+v` on macOS or `control+v` on Windows/Linux with Computer Use.
|
||||
9. **Insert Images**: for each `contentImages` item in placeholder order:
|
||||
- Run `copy-to-clipboard.ts image <localPath>`.
|
||||
- Select the exact placeholder text (`XIMGPH_N`) in the editor.
|
||||
- Press `super+v`/`control+v` with Computer Use.
|
||||
- Wait for X to finish uploading media.
|
||||
- If `XIMGPH_N` remains above the inserted image, reselect that exact text and press `BackSpace`.
|
||||
- Do not press `BackSpace` unless the Computer Use state confirms the selected text is exactly the placeholder.
|
||||
10. **Verify**:
|
||||
- Inspect the Computer Use state for `XIMGPH_` residue.
|
||||
- Confirm the expected number of image blocks is visible.
|
||||
- Open Preview and verify title, cover, body, links, and images.
|
||||
11. **Publish Safety**: ask the user for explicit final confirmation before clicking `Publish`.
|
||||
|
||||
## CDP Script Workflow (Fallback)
|
||||
|
||||
1. **Parse Markdown**: Extract title, cover, content images, generate HTML
|
||||
2. **Launch Chrome**: Real browser with CDP, persistent login
|
||||
@@ -137,7 +194,7 @@ JSON output:
|
||||
- Compare expected vs actual image count
|
||||
- Warn if issues found
|
||||
10. **Review**: Browser stays open for 60s preview
|
||||
11. **Publish**: Only with `--submit` flag
|
||||
11. **Publish**: Only with `--submit` flag and explicit user confirmation
|
||||
|
||||
## Example Session
|
||||
|
||||
@@ -145,15 +202,15 @@ JSON output:
|
||||
User: /post-to-x article ./blog/my-post.md --cover ./thumbnail.png
|
||||
|
||||
Claude:
|
||||
1. Parses markdown: title="My Post", 3 content images
|
||||
2. Launches Chrome with CDP
|
||||
3. Navigates to x.com/compose/articles
|
||||
4. Clicks create button
|
||||
1. Detects Chrome Computer Use
|
||||
2. Parses markdown: title="My Post", 3 content images
|
||||
3. Saves `/tmp/x-article-body.html` and `/tmp/x-article.json`
|
||||
4. Uses Chrome Computer Use to open X Articles and create a draft
|
||||
5. Uploads thumbnail.png as cover
|
||||
6. Fills title "My Post"
|
||||
7. Pastes HTML content
|
||||
7. Pastes HTML content with a real Chrome paste
|
||||
8. Inserts 3 images at placeholder positions
|
||||
9. Reports: "Article composed. Review and use --submit to publish."
|
||||
9. Opens Preview and asks before publishing
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
@@ -162,6 +219,8 @@ Claude:
|
||||
- **Cover upload fails**: Check file path and format (PNG, JPEG)
|
||||
- **Images not inserting**: Verify placeholders exist in pasted content
|
||||
- **Content not pasting**: Check HTML clipboard: `${BUN_X} {baseDir}/scripts/copy-to-clipboard.ts html --file /tmp/test.html`
|
||||
- **Computer Use unavailable**: Use the CDP fallback script, unless the user explicitly required Chrome Computer Use.
|
||||
- **Placeholder remains after paste**: Select only the placeholder text and press BackSpace after upload completes.
|
||||
|
||||
## How It Works
|
||||
|
||||
@@ -172,7 +231,13 @@ Claude:
|
||||
- Downloads remote images locally
|
||||
- Returns structured JSON
|
||||
|
||||
2. `x-article.ts` publishes via CDP:
|
||||
2. Chrome Computer Use publishes through the user's visible Chrome UI:
|
||||
- Uses the user's active Chrome profile and logged-in X session
|
||||
- Uses `copy-to-clipboard.ts` for rich HTML and image clipboard payloads
|
||||
- Uses real keystrokes (`super+v`/`control+v`) through Codex Computer Use
|
||||
- Keeps the final publish click under user confirmation
|
||||
|
||||
3. `x-article.ts` publishes via CDP as a fallback:
|
||||
- Launches real Chrome (bypasses detection)
|
||||
- Uses persistent profile (saved login)
|
||||
- Navigates and fills editor via DOM manipulation
|
||||
|
||||
@@ -6,6 +6,17 @@ Detailed documentation for posting text and images to X.
|
||||
|
||||
If you prefer step-by-step control:
|
||||
|
||||
### Step 0: Prefer Chrome Computer Use in Codex
|
||||
|
||||
When running inside Codex, first detect whether Chrome Computer Use is enabled:
|
||||
|
||||
1. If Computer Use tools are already visible, call `get_app_state` for `Google Chrome`.
|
||||
2. If not, use `tool_search` for `computer-use get_app_state click press_key drag scroll Google Chrome`, then call `get_app_state`.
|
||||
3. If `get_app_state` succeeds, use the user's real Chrome with Computer Use for all X UI actions.
|
||||
4. Use CDP scripts only when Computer Use is unavailable or explicitly requested.
|
||||
|
||||
If the user explicitly asks for Chrome Computer Use, do not use Playwright, the in-app Browser, or CDP without approval.
|
||||
|
||||
### Step 1: Copy Image to Clipboard
|
||||
|
||||
```bash
|
||||
@@ -25,27 +36,15 @@ ${BUN_X} {baseDir}/scripts/paste-from-clipboard.ts --app "Google Chrome" --retri
|
||||
${BUN_X} {baseDir}/scripts/paste-from-clipboard.ts --delay 200
|
||||
```
|
||||
|
||||
### Step 3: Use Playwright MCP (if Chrome session available)
|
||||
### Step 3: Use Chrome Computer Use (Preferred)
|
||||
|
||||
```bash
|
||||
# Navigate
|
||||
mcp__playwright__browser_navigate url="https://x.com/compose/post"
|
||||
|
||||
# Get element refs
|
||||
mcp__playwright__browser_snapshot
|
||||
|
||||
# Type text
|
||||
mcp__playwright__browser_click element="editor" ref="<ref>"
|
||||
mcp__playwright__browser_type element="editor" ref="<ref>" text="Your content"
|
||||
|
||||
# Paste image (after copying to clipboard)
|
||||
mcp__playwright__browser_press_key key="Meta+v" # macOS
|
||||
# or
|
||||
mcp__playwright__browser_press_key key="Control+v" # Windows/Linux
|
||||
|
||||
# Screenshot to verify
|
||||
mcp__playwright__browser_take_screenshot filename="preview.png"
|
||||
```
|
||||
1. Use `get_app_state` for `Google Chrome`.
|
||||
2. Navigate Chrome to `https://x.com/compose/post` if needed.
|
||||
3. Click the composer and type the post text.
|
||||
4. Copy each image to the clipboard with `copy-to-clipboard.ts image <path>`.
|
||||
5. Press `super+v` on macOS or `control+v` on Windows/Linux with Computer Use.
|
||||
6. Wait until X finishes media upload.
|
||||
7. Ask for explicit confirmation before clicking `Post`.
|
||||
|
||||
## Image Support
|
||||
|
||||
@@ -59,12 +58,12 @@ mcp__playwright__browser_take_screenshot filename="preview.png"
|
||||
User: /post-to-x "Hello from Claude!" --image ./screenshot.png
|
||||
|
||||
Claude:
|
||||
1. Runs: ${BUN_X} {baseDir}/scripts/x-browser.ts "Hello from Claude!" --image ./screenshot.png
|
||||
2. Chrome opens with X compose page
|
||||
3. Text is typed into editor
|
||||
4. Image is copied to clipboard and pasted
|
||||
5. Browser stays open 30s for preview
|
||||
6. Reports: "Post composed. Use --submit to post."
|
||||
1. Detects Chrome Computer Use
|
||||
2. Opens X compose in the user's real Chrome
|
||||
3. Types text into editor
|
||||
4. Copies image to clipboard and pastes with Computer Use
|
||||
5. Waits for upload and verifies the preview
|
||||
6. Asks before clicking Post
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
@@ -80,7 +79,13 @@ Claude:
|
||||
|
||||
## How It Works
|
||||
|
||||
The `x-browser.ts` script uses Chrome DevTools Protocol (CDP) to:
|
||||
In Chrome Computer Use mode:
|
||||
1. Codex controls the user's visible Google Chrome window
|
||||
2. Text is typed through the real UI
|
||||
3. Images are copied to the system clipboard and pasted with real keystrokes
|
||||
4. The user confirms before the final public post
|
||||
|
||||
The `x-browser.ts` script is the CDP fallback. It uses Chrome DevTools Protocol (CDP) to:
|
||||
1. Launch real Chrome (not Playwright) with `--disable-blink-features=AutomationControlled`
|
||||
2. Use persistent profile directory for saved login sessions
|
||||
3. Interact with X via CDP commands (Runtime.evaluate, Input.dispatchKeyEvent)
|
||||
|
||||
@@ -5,6 +5,7 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
import frontMatter from 'front-matter';
|
||||
import hljs from 'highlight.js/lib/common';
|
||||
@@ -458,7 +459,9 @@ async function main(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
await main().catch((err) => {
|
||||
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
process.exit(1);
|
||||
});
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
await main().catch((err) => {
|
||||
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user