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:
parent
5acef7151b
commit
88b433d565
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
- : 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
|
- : likely text-heavy framework graphic, check whether labels need translation
|
||||||
```
|
```
|
||||||
|
|
||||||
Display summary:
|
Display summary:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue