Compare commits

...

5 Commits

Author SHA1 Message Date
Jim Liu 宝玉 505a7e10ce chore: release v1.113.0 2026-04-25 15:03:22 -05:00
Jim Liu 宝玉 6d063734ae feat(baoyu-imagine): add DashScope Wan 2.7 image model support (#141)
* feat(baoyu-imagine): add DashScope Wan 2.7 image model support

Closes #139.

Adds the new `wan2.7-image-pro` and `wan2.7-image` model family to the
DashScope provider so users can call Wan 2.7 directly through the
official Aliyun (Bailian) API instead of going through Replicate.

- Register `wan2.7-image-pro` and `wan2.7-image` as a new `wan27` family
  in the DashScope provider with their own size resolution rules:
  pixel range `[768*768, 4096*4096]` for `wan2.7-image-pro` text-to-image,
  `[768*768, 2048*2048]` for `wan2.7-image-pro` with refs and for the
  base `wan2.7-image` model in any mode, with aspect ratios validated
  against the documented `[1:8, 8:1]` band.
- Allow up to 9 reference images per request (image editing /
  multi-image fusion). Local files are inlined as base64 data URLs;
  `http(s)://` paths are forwarded as-is. Other DashScope models still
  reject `--ref` with a hint to switch to a wan2.7 model or another
  provider.
- Drop `prompt_extend` from the request body for the Wan 2.7 family
  (not part of the Wan 2.7 API surface) and skip the Qwen-only negative
  prompt for this family.
- Allow `--provider dashscope --ref ...` in `detectProvider` so users
  can opt into Wan 2.7 reference workflows, while keeping Wan 2.7 out
  of the auto-detect ref priority list.
- Add provider, reference, and usage-example documentation, plus
  unit tests covering family routing, size derivation across the
  three pixel-budget modes, ratio rejection, explicit-size validation,
  and the new `--provider dashscope` ref opt-in path.

Made-with: Cursor

* fix(baoyu-imagine): force n=1 for DashScope wan2.7 to avoid silent multi-image billing

Cross-checked the implementation against the official Wan 2.7 image
generation & editing API reference and found that the API defaults
`parameters.n` to 4 in non-collage mode (1-4 range, billed per image).
baoyu-imagine has single-image save semantics — only the first image
in the response is kept — so without an explicit `n: 1` users would
silently pay for 3 discarded images per request.

- Always send `parameters.n: 1` in the wan2.7 request body
- Reject `--n > 1` for wan2.7 with a clear error pointing at the
  single-image save semantics
- Add tests asserting the request body shape (n=1, no prompt_extend,
  no negative_prompt) and the --n>1 rejection
- Document the defaults-vs-skill mismatch in the dashscope reference

Made-with: Cursor

* Fix DashScope Wan 2.7 review feedback
2026-04-25 14:54:08 -05:00
Jim Liu 宝玉 31d728b505 chore: release v1.112.0 2026-04-24 02:15:57 -05:00
Jim Liu 宝玉 f6d5df0594 fix(baoyu-post-to-x): add entry point guard to md-to-html.ts for module import compatibility
Wrap main() in an import.meta.url check 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.
2026-04-24 02:15:30 -05:00
Jim Liu 宝玉 4bd5fe573e feat(baoyu-article-illustrator): default to sketch-notes educational infographic style
Make `hand-drawn-edu` (infographic + sketch-notes + macaron) the universal
fallback preset when content analysis surfaces no strong signal. Rework
sketch-notes style spec around warm cream paper + black hand-drawn lines +
soft pastel section blocks.

- Change `hand-drawn-edu` preset type from flowchart to infographic; add
  `hand-drawn-edu-flow` (flowchart) and `hand-drawn-edu-compare` (comparison)
  as variants for users who need those layouts in the same warm style
- Elevate `sketch-notes` to primary style across infographic / flowchart /
  comparison / framework auto-selection; add sketch-notes column to
  Type x Style compatibility matrix
- Rewrite sketch-notes.md: macaron pastel palette, canonical single-page
  layout (title / sectioned boxes / takeaway), diagram-only rule
- Add infographic + sketch-notes + macaron prompt block to
  prompt-construction.md
- Update workflow, style-presets, and first-time-setup defaults to match
2026-04-24 02:15:25 -05:00
20 changed files with 747 additions and 85 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
},
"metadata": {
"description": "Skills shared by Baoyu for improving daily work efficiency",
"version": "1.111.1"
"version": "1.113.0"
},
"plugins": [
{
+17
View File
@@ -2,6 +2,23 @@
English | [中文](./CHANGELOG.zh.md)
## 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
+17
View File
@@ -2,6 +2,23 @@
[English](./CHANGELOG.md) | 中文
## 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
### 文档
+1 -1
View File
@@ -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: 26 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
- 26 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 (26 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 (1015%)** — Bold hand-lettered title, optionally with a small decorative underline or doodle.
2. **Middle (7080%)** — 26 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 (1015%)** — 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.
+1 -1
View File
@@ -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
+64 -2
View File
@@ -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);
});
+41 -7
View File
@@ -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 -1
View File
@@ -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
version: 1.56.2
metadata:
openclaw:
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-post-to-x
+7 -4
View File
@@ -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);
});
}