docs: improve skill documentation clarity

- Fix Provider Selection: default to Replicate when multiple keys available (matches code)
- Add Replicate column to Quality Presets table (normal→1K, 2k→2K)
- Add Replicate aspect ratio behavior (match_input_image when --ref without --ar)
- Remove stale Google Imagen reference from Aspect Ratios
- Add batch file format example with JSON schema to SKILL.md
- Note that batch paths resolve relative to batch file directory
- Move batch execution strategy in article-illustrator before numbered steps
- Fix translate image-language reminder to use standard markdown syntax
  with note to match article's own image syntax convention
This commit is contained in:
Jim Liu 宝玉 2026-03-09 00:43:05 -05:00
parent 5acef7151b
commit 88b433d565
6 changed files with 132 additions and 35 deletions

View File

@ -118,6 +118,8 @@ Full template: [references/workflow.md](references/workflow.md#step-4-generate-o
⛔ **BLOCKING: Prompt files MUST be saved before ANY image generation.** ⛔ **BLOCKING: Prompt files MUST be saved before ANY image generation.**
**Execution strategy**: When multiple illustrations have saved prompt files and the task is now plain generation, prefer `baoyu-image-gen` batch mode (`build-batch.ts` → `--batchfile`) over spawning subagents. Use subagents only when each image still needs separate prompt iteration or creative exploration.
1. For each illustration, create a prompt file per [references/prompt-construction.md](references/prompt-construction.md) 1. For each illustration, create a prompt file per [references/prompt-construction.md](references/prompt-construction.md)
2. Save to `prompts/NN-{type}-{slug}.md` with YAML frontmatter 2. Save to `prompts/NN-{type}-{slug}.md` with YAML frontmatter
3. Prompts **MUST** use type-specific templates with structured sections (ZONES / LABELS / COLORS / STYLE / ASPECT) 3. Prompts **MUST** use type-specific templates with structured sections (ZONES / LABELS / COLORS / STYLE / ASPECT)

View File

@ -315,6 +315,10 @@ Prompt Files:
**DO NOT** pass ad-hoc inline text to `--prompt` without first saving prompt files. The generation command should either use `--promptfiles prompts/NN-{type}-{slug}.md` or read the saved file content for `--prompt`. **DO NOT** pass ad-hoc inline text to `--prompt` without first saving prompt files. The generation command should either use `--promptfiles prompts/NN-{type}-{slug}.md` or read the saved file content for `--prompt`.
**Execution choice**:
- If multiple illustrations already have saved prompt files and the task is now plain generation, prefer `baoyu-image-gen` batch mode (`build-batch.ts` -> `main.ts --batchfile`)
- Use subagents only when each illustration still needs separate prompt rewriting, style exploration, or other per-image reasoning before generation
**CRITICAL - References in Frontmatter**: **CRITICAL - References in Frontmatter**:
- Only add `references` field if files ACTUALLY EXIST in `references/` directory - Only add `references` field if files ACTUALLY EXIST in `references/` directory
- If style/palette was extracted verbally (no file), append info to prompt BODY instead - If style/palette was extracted verbally (no file), append info to prompt BODY instead

View File

@ -1,6 +1,6 @@
--- ---
name: baoyu-image-gen name: baoyu-image-gen
description: AI image generation with OpenAI, Google, DashScope and Replicate APIs. Supports text-to-image, reference images, aspect ratios. Sequential by default; parallel generation available on request. Use when user asks to generate, create, or draw images. description: AI image generation with OpenAI, Google, DashScope and Replicate APIs. Supports text-to-image, reference images, aspect ratios, and batch generation from saved prompt files. Sequential by default; use batch parallel generation when the user already has multiple prompts or wants stable multi-image throughput. Use when user asks to generate, create, or draw images.
version: 1.56.1 version: 1.56.1
metadata: metadata:
openclaw: openclaw:
@ -99,6 +99,33 @@ ${BUN_X} {baseDir}/scripts/main.ts --batchfile batch.json
${BUN_X} {baseDir}/scripts/main.ts --batchfile batch.json --jobs 4 --json ${BUN_X} {baseDir}/scripts/main.ts --batchfile batch.json --jobs 4 --json
``` ```
### Batch File Format
```json
{
"jobs": 4,
"tasks": [
{
"id": "hero",
"promptFiles": ["prompts/hero.md"],
"image": "out/hero.png",
"provider": "replicate",
"model": "google/nano-banana-pro",
"ar": "16:9",
"quality": "2k"
},
{
"id": "diagram",
"promptFiles": ["prompts/diagram.md"],
"image": "out/diagram.png",
"ref": ["references/original.png"]
}
]
}
```
Paths in `promptFiles`, `image`, and `ref` are resolved relative to the batch file's directory. `jobs` is optional (overridden by CLI `--jobs`). Top-level array format (without `jobs` wrapper) is also accepted.
## Options ## Options
| Option | Description | | Option | Description |
@ -177,14 +204,14 @@ ${BUN_X} {baseDir}/scripts/main.ts --prompt "A cat" --image out.png --provider r
1. `--ref` provided + no `--provider` → auto-select Google first, then OpenAI, then Replicate 1. `--ref` provided + no `--provider` → auto-select Google first, then OpenAI, then Replicate
2. `--provider` specified → use it (if `--ref`, must be `google`, `openai`, or `replicate`) 2. `--provider` specified → use it (if `--ref`, must be `google`, `openai`, or `replicate`)
3. Only one API key available → use that provider 3. Only one API key available → use that provider
4. Multiple available → default to Google 4. Multiple available → default to Replicate (`google/nano-banana-pro`)
## Quality Presets ## Quality Presets
| Preset | Google imageSize | OpenAI Size | Use Case | | Preset | Google imageSize | OpenAI Size | Replicate resolution | Use Case |
|--------|------------------|-------------|----------| |--------|------------------|-------------|----------------------|----------|
| `normal` | 1K | 1024px | Quick previews | | `normal` | 1K | 1024px | 1K | Quick previews |
| `2k` (default) | 2K | 2048px | Covers, illustrations, infographics | | `2k` (default) | 2K | 2048px | 2K | Covers, illustrations, infographics |
**Google imageSize**: Can be overridden with `--imageSize 1K|2K|4K` **Google imageSize**: Can be overridden with `--imageSize 1K|2K|4K`
@ -193,8 +220,8 @@ ${BUN_X} {baseDir}/scripts/main.ts --prompt "A cat" --image out.png --provider r
Supported: `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `2.35:1` Supported: `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `2.35:1`
- Google multimodal: uses `imageConfig.aspectRatio` - Google multimodal: uses `imageConfig.aspectRatio`
- Google Imagen: uses `aspectRatio` parameter
- OpenAI: maps to closest supported size - OpenAI: maps to closest supported size
- Replicate: passes `aspect_ratio` to model; when `--ref` is provided without `--ar`, defaults to `match_input_image`
## Generation Mode ## Generation Mode
@ -207,6 +234,20 @@ Supported: `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `2.35:1`
| Sequential (default) | Normal usage, single images, small batches | | Sequential (default) | Normal usage, single images, small batches |
| Parallel batch | Batch mode with 2+ tasks | | Parallel batch | Batch mode with 2+ tasks |
Execution choice:
| Situation | Preferred approach | Why |
|-----------|--------------------|-----|
| One image, or 1-2 simple images | Sequential | Lower coordination overhead and easier debugging |
| Multiple images already have saved prompt files | Batch (`--batchfile`) | Reuses finalized prompts, applies shared throttling/retries, and gives predictable throughput |
| Each image still needs separate reasoning, prompt writing, or style exploration | Subagents | The work is still exploratory, so each image may need independent analysis before generation |
| Output comes from `baoyu-article-illustrator` with `outline.md` + `prompts/` | Batch (`build-batch.ts` -> `--batchfile`) | That workflow already produces prompt files, so direct batch execution is the intended path |
Rule of thumb:
- Prefer batch over subagents once prompt files are already saved and the task is "generate all of these"
- Use subagents only when generation is coupled with per-image thinking, rewriting, or divergent creative exploration
Parallel behavior: Parallel behavior:
- Default worker count is automatic, capped by config, built-in default 10 - Default worker count is automatic, capped by config, built-in default 10

View File

@ -40,6 +40,12 @@ type ProviderRateLimit = {
startIntervalMs: number; startIntervalMs: number;
}; };
type LoadedBatchTasks = {
tasks: BatchTaskInput[];
jobs: number | null;
batchDir: string;
};
const MAX_ATTEMPTS = 3; const MAX_ATTEMPTS = 3;
const DEFAULT_MAX_WORKERS = 10; const DEFAULT_MAX_WORKERS = 10;
const POLL_WAIT_MS = 250; const POLL_WAIT_MS = 250;
@ -74,16 +80,19 @@ Options:
-h, --help Show help -h, --help Show help
Batch file format: Batch file format:
[ {
{ "jobs": 4,
"id": "hero", "tasks": [
"promptFiles": ["prompts/hero.md"], {
"image": "out/hero.png", "id": "hero",
"provider": "replicate", "promptFiles": ["prompts/hero.md"],
"model": "google/nano-banana-pro", "image": "out/hero.png",
"ar": "16:9" "provider": "replicate",
} "model": "google/nano-banana-pro",
] "ar": "16:9"
}
]
}
Behavior: Behavior:
- Batch mode automatically runs in parallel when pending tasks >= 2 - Batch mode automatically runs in parallel when pending tasks >= 2
@ -433,6 +442,17 @@ function parsePositiveInt(value: string | undefined): number | null {
return Number.isFinite(parsed) && parsed > 0 ? parsed : null; return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
} }
function parsePositiveBatchInt(value: unknown): number | null {
if (value === null || value === undefined) return null;
if (typeof value === "number") {
return Number.isInteger(value) && value > 0 ? value : null;
}
if (typeof value === "string") {
return parsePositiveInt(value);
}
return null;
}
function getConfiguredMaxWorkers(extendConfig: Partial<ExtendConfig>): number { function getConfiguredMaxWorkers(extendConfig: Partial<ExtendConfig>): number {
const envValue = parsePositiveInt(process.env.BAOYU_IMAGE_GEN_MAX_WORKERS); const envValue = parsePositiveInt(process.env.BAOYU_IMAGE_GEN_MAX_WORKERS);
const configValue = extendConfig.batch?.max_workers ?? null; const configValue = extendConfig.batch?.max_workers ?? null;
@ -626,27 +646,49 @@ async function prepareSingleTask(args: CliArgs, extendConfig: Partial<ExtendConf
}; };
} }
async function loadBatchTasks(batchFilePath: string): Promise<BatchTaskInput[]> { async function loadBatchTasks(batchFilePath: string): Promise<LoadedBatchTasks> {
const content = await readFile(path.resolve(batchFilePath), "utf8"); const resolvedBatchFilePath = path.resolve(batchFilePath);
const content = await readFile(resolvedBatchFilePath, "utf8");
const parsed = JSON.parse(content.replace(/^\uFEFF/, "")) as BatchFile; const parsed = JSON.parse(content.replace(/^\uFEFF/, "")) as BatchFile;
if (Array.isArray(parsed)) return parsed; const batchDir = path.dirname(resolvedBatchFilePath);
if (parsed && typeof parsed === "object" && Array.isArray(parsed.tasks)) return parsed.tasks; if (Array.isArray(parsed)) {
return {
tasks: parsed,
jobs: null,
batchDir,
};
}
if (parsed && typeof parsed === "object" && Array.isArray(parsed.tasks)) {
const jobs = parsePositiveBatchInt(parsed.jobs);
if (parsed.jobs !== undefined && parsed.jobs !== null && jobs === null) {
throw new Error("Invalid batch file. jobs must be a positive integer when provided.");
}
return {
tasks: parsed.tasks,
jobs,
batchDir,
};
}
throw new Error("Invalid batch file. Expected an array of tasks or an object with a tasks array."); throw new Error("Invalid batch file. Expected an array of tasks or an object with a tasks array.");
} }
function createTaskArgs(baseArgs: CliArgs, task: BatchTaskInput): CliArgs { function resolveBatchPath(batchDir: string, filePath: string): string {
return path.isAbsolute(filePath) ? filePath : path.resolve(batchDir, filePath);
}
function createTaskArgs(baseArgs: CliArgs, task: BatchTaskInput, batchDir: string): CliArgs {
return { return {
...baseArgs, ...baseArgs,
prompt: task.prompt ?? null, prompt: task.prompt ?? null,
promptFiles: task.promptFiles ? [...task.promptFiles] : [], promptFiles: task.promptFiles ? task.promptFiles.map((filePath) => resolveBatchPath(batchDir, filePath)) : [],
imagePath: task.image ?? null, imagePath: task.image ? resolveBatchPath(batchDir, task.image) : null,
provider: task.provider ?? baseArgs.provider ?? null, provider: task.provider ?? baseArgs.provider ?? null,
model: task.model ?? baseArgs.model ?? null, model: task.model ?? baseArgs.model ?? null,
aspectRatio: task.ar ?? baseArgs.aspectRatio ?? null, aspectRatio: task.ar ?? baseArgs.aspectRatio ?? null,
size: task.size ?? baseArgs.size ?? null, size: task.size ?? baseArgs.size ?? null,
quality: task.quality ?? baseArgs.quality ?? null, quality: task.quality ?? baseArgs.quality ?? null,
imageSize: task.imageSize ?? baseArgs.imageSize ?? null, imageSize: task.imageSize ?? baseArgs.imageSize ?? null,
referenceImages: task.ref ? [...task.ref] : [], referenceImages: task.ref ? task.ref.map((filePath) => resolveBatchPath(batchDir, filePath)) : [],
n: task.n ?? baseArgs.n, n: task.n ?? baseArgs.n,
batchFile: null, batchFile: null,
jobs: baseArgs.jobs, jobs: baseArgs.jobs,
@ -658,15 +700,15 @@ function createTaskArgs(baseArgs: CliArgs, task: BatchTaskInput): CliArgs {
async function prepareBatchTasks( async function prepareBatchTasks(
args: CliArgs, args: CliArgs,
extendConfig: Partial<ExtendConfig> extendConfig: Partial<ExtendConfig>
): Promise<PreparedTask[]> { ): Promise<{ tasks: PreparedTask[]; jobs: number | null }> {
if (!args.batchFile) throw new Error("--batchfile is required in batch mode"); if (!args.batchFile) throw new Error("--batchfile is required in batch mode");
const taskInputs = await loadBatchTasks(args.batchFile); const { tasks: taskInputs, jobs: batchJobs, batchDir } = await loadBatchTasks(args.batchFile);
if (taskInputs.length === 0) throw new Error("Batch file does not contain any tasks."); if (taskInputs.length === 0) throw new Error("Batch file does not contain any tasks.");
const prepared: PreparedTask[] = []; const prepared: PreparedTask[] = [];
for (let i = 0; i < taskInputs.length; i++) { for (let i = 0; i < taskInputs.length; i++) {
const task = taskInputs[i]!; const task = taskInputs[i]!;
const taskArgs = createTaskArgs(args, task); const taskArgs = createTaskArgs(args, task, batchDir);
const prompt = await loadPromptForArgs(taskArgs); const prompt = await loadPromptForArgs(taskArgs);
if (!prompt) throw new Error(`Task ${i + 1} is missing prompt or promptFiles.`); 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.imagePath) throw new Error(`Task ${i + 1} is missing image output path.`);
@ -686,7 +728,10 @@ async function prepareBatchTasks(
}); });
} }
return prepared; return {
tasks: prepared,
jobs: args.jobs ?? batchJobs,
};
} }
async function writeImage(outputPath: string, imageData: Uint8Array): Promise<void> { async function writeImage(outputPath: string, imageData: Uint8Array): Promise<void> {
@ -861,8 +906,8 @@ async function runSingleMode(args: CliArgs, extendConfig: Partial<ExtendConfig>)
} }
async function runBatchMode(args: CliArgs, extendConfig: Partial<ExtendConfig>): Promise<void> { async function runBatchMode(args: CliArgs, extendConfig: Partial<ExtendConfig>): Promise<void> {
const tasks = await prepareBatchTasks(args, extendConfig); const { tasks, jobs } = await prepareBatchTasks(args, extendConfig);
const results = await runBatchTasks(tasks, args.jobs, extendConfig); const results = await runBatchTasks(tasks, jobs, extendConfig);
printBatchSummary(results); printBatchSummary(results);
if (args.json) { if (args.json) {

View File

@ -34,7 +34,12 @@ export type BatchTaskInput = {
n?: number; n?: number;
}; };
export type BatchFile = BatchTaskInput[] | { tasks: BatchTaskInput[] }; export type BatchFile =
| BatchTaskInput[]
| {
tasks: BatchTaskInput[];
jobs?: number | null;
};
export type ExtendConfig = { export type ExtendConfig = {
version: number; version: number;

View File

@ -258,11 +258,11 @@ After the final translation is written, do a lightweight image-language pass:
3. If any image likely contains a main text language that does not match the translated article language, proactively remind the user 3. If any image likely contains a main text language that does not match the translated article language, proactively remind the user
4. The reminder must be a list only. Do not automatically localize those images unless the user asks 4. The reminder must be a list only. Do not automatically localize those images unless the user asks
Reminder format: Reminder format (use whatever image syntax the article already uses — standard markdown or wikilink):
```text ```text
Possible image localization needed: Possible image localization needed:
- ![[attachments/example-cover.png]]: likely still contains source-language text while the article is now in target language - ![example cover](attachments/example-cover.png): likely still contains source-language text while the article is now in target language
- ![[attachments/example-diagram.png]]: likely text-heavy framework graphic, check whether labels need translation - ![example diagram](attachments/example-diagram.png): likely text-heavy framework graphic, check whether labels need translation
``` ```
Display summary: Display summary: