diff --git a/skills/baoyu-article-illustrator/SKILL.md b/skills/baoyu-article-illustrator/SKILL.md index 1c7457e..80fa001 100644 --- a/skills/baoyu-article-illustrator/SKILL.md +++ b/skills/baoyu-article-illustrator/SKILL.md @@ -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.** +**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) 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) diff --git a/skills/baoyu-article-illustrator/references/workflow.md b/skills/baoyu-article-illustrator/references/workflow.md index 53efc27..c7bfc99 100644 --- a/skills/baoyu-article-illustrator/references/workflow.md +++ b/skills/baoyu-article-illustrator/references/workflow.md @@ -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`. +**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**: - 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 diff --git a/skills/baoyu-image-gen/SKILL.md b/skills/baoyu-image-gen/SKILL.md index 85fac82..87ea8fc 100644 --- a/skills/baoyu-image-gen/SKILL.md +++ b/skills/baoyu-image-gen/SKILL.md @@ -1,6 +1,6 @@ --- 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 metadata: 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 ``` +### 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 | 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 2. `--provider` specified → use it (if `--ref`, must be `google`, `openai`, or `replicate`) 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 -| Preset | Google imageSize | OpenAI Size | Use Case | -|--------|------------------|-------------|----------| -| `normal` | 1K | 1024px | Quick previews | -| `2k` (default) | 2K | 2048px | Covers, illustrations, infographics | +| Preset | Google imageSize | OpenAI Size | Replicate resolution | Use Case | +|--------|------------------|-------------|----------------------|----------| +| `normal` | 1K | 1024px | 1K | Quick previews | +| `2k` (default) | 2K | 2048px | 2K | Covers, illustrations, infographics | **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` - Google multimodal: uses `imageConfig.aspectRatio` -- Google Imagen: uses `aspectRatio` parameter - 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 @@ -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 | | 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: - Default worker count is automatic, capped by config, built-in default 10 diff --git a/skills/baoyu-image-gen/scripts/main.ts b/skills/baoyu-image-gen/scripts/main.ts index b2ba3d8..9ef6e9c 100644 --- a/skills/baoyu-image-gen/scripts/main.ts +++ b/skills/baoyu-image-gen/scripts/main.ts @@ -40,6 +40,12 @@ type ProviderRateLimit = { startIntervalMs: number; }; +type LoadedBatchTasks = { + tasks: BatchTaskInput[]; + jobs: number | null; + batchDir: string; +}; + const MAX_ATTEMPTS = 3; const DEFAULT_MAX_WORKERS = 10; const POLL_WAIT_MS = 250; @@ -74,16 +80,19 @@ Options: -h, --help Show help Batch file format: - [ - { - "id": "hero", - "promptFiles": ["prompts/hero.md"], - "image": "out/hero.png", - "provider": "replicate", - "model": "google/nano-banana-pro", - "ar": "16:9" - } - ] + { + "jobs": 4, + "tasks": [ + { + "id": "hero", + "promptFiles": ["prompts/hero.md"], + "image": "out/hero.png", + "provider": "replicate", + "model": "google/nano-banana-pro", + "ar": "16:9" + } + ] + } Behavior: - 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; } +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): number { const envValue = parsePositiveInt(process.env.BAOYU_IMAGE_GEN_MAX_WORKERS); const configValue = extendConfig.batch?.max_workers ?? null; @@ -626,27 +646,49 @@ async function prepareSingleTask(args: CliArgs, extendConfig: Partial { - const content = await readFile(path.resolve(batchFilePath), "utf8"); +async function loadBatchTasks(batchFilePath: string): Promise { + const resolvedBatchFilePath = path.resolve(batchFilePath); + const content = await readFile(resolvedBatchFilePath, "utf8"); const parsed = JSON.parse(content.replace(/^\uFEFF/, "")) as BatchFile; - if (Array.isArray(parsed)) return parsed; - if (parsed && typeof parsed === "object" && Array.isArray(parsed.tasks)) return parsed.tasks; + const batchDir = path.dirname(resolvedBatchFilePath); + 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."); } -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 { ...baseArgs, prompt: task.prompt ?? null, - promptFiles: task.promptFiles ? [...task.promptFiles] : [], - imagePath: task.image ?? null, + promptFiles: task.promptFiles ? task.promptFiles.map((filePath) => resolveBatchPath(batchDir, filePath)) : [], + imagePath: task.image ? resolveBatchPath(batchDir, task.image) : null, provider: task.provider ?? baseArgs.provider ?? null, model: task.model ?? baseArgs.model ?? null, aspectRatio: task.ar ?? baseArgs.aspectRatio ?? null, size: task.size ?? baseArgs.size ?? null, quality: task.quality ?? baseArgs.quality ?? 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, batchFile: null, jobs: baseArgs.jobs, @@ -658,15 +700,15 @@ function createTaskArgs(baseArgs: CliArgs, task: BatchTaskInput): CliArgs { async function prepareBatchTasks( args: CliArgs, extendConfig: Partial -): Promise { +): Promise<{ tasks: PreparedTask[]; jobs: number | null }> { 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."); const prepared: PreparedTask[] = []; for (let i = 0; i < taskInputs.length; i++) { const task = taskInputs[i]!; - const taskArgs = createTaskArgs(args, task); + const taskArgs = createTaskArgs(args, task, batchDir); 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.`); @@ -686,7 +728,10 @@ async function prepareBatchTasks( }); } - return prepared; + return { + tasks: prepared, + jobs: args.jobs ?? batchJobs, + }; } async function writeImage(outputPath: string, imageData: Uint8Array): Promise { @@ -861,8 +906,8 @@ async function runSingleMode(args: CliArgs, extendConfig: Partial) } async function runBatchMode(args: CliArgs, extendConfig: Partial): Promise { - const tasks = await prepareBatchTasks(args, extendConfig); - const results = await runBatchTasks(tasks, args.jobs, extendConfig); + const { tasks, jobs } = await prepareBatchTasks(args, extendConfig); + const results = await runBatchTasks(tasks, jobs, extendConfig); printBatchSummary(results); if (args.json) { diff --git a/skills/baoyu-image-gen/scripts/types.ts b/skills/baoyu-image-gen/scripts/types.ts index 516d3a1..e3616d3 100644 --- a/skills/baoyu-image-gen/scripts/types.ts +++ b/skills/baoyu-image-gen/scripts/types.ts @@ -34,7 +34,12 @@ export type BatchTaskInput = { n?: number; }; -export type BatchFile = BatchTaskInput[] | { tasks: BatchTaskInput[] }; +export type BatchFile = + | BatchTaskInput[] + | { + tasks: BatchTaskInput[]; + jobs?: number | null; + }; export type ExtendConfig = { version: number; diff --git a/skills/baoyu-translate/SKILL.md b/skills/baoyu-translate/SKILL.md index 2adbe84..deb1263 100644 --- a/skills/baoyu-translate/SKILL.md +++ b/skills/baoyu-translate/SKILL.md @@ -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 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 Possible image localization needed: -- ![[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 cover](attachments/example-cover.png): likely still contains source-language text while the article is now in target language +- ![example diagram](attachments/example-diagram.png): likely text-heavy framework graphic, check whether labels need translation ``` Display summary: