Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Generate images via OpenAI, Google, OpenRouter, DashScope, Jimeng, Seedream, and Replicate APIs with batch support.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/build-batch.ts
1import path from "node:path";2import process from "node:process";3import { readdir, readFile, writeFile } from "node:fs/promises";45type CliArgs = {6outlinePath: string | null;7promptsDir: string | null;8outputPath: string | null;9imagesDir: string | null;10refsDir: string;11provider: string;12model: string | null;13aspectRatio: string;14quality: string;15jobs: number | null;16help: boolean;17};1819type OutlineEntry = {20index: number;21filename: string;22};2324type PromptReference = {25filename: string;26usage: "direct" | "style" | "palette";27};2829function printUsage(): void {30console.log(`Usage:31bun <baseDir>/scripts/build-batch.ts --outline outline.md --prompts prompts --output batch.json --images-dir attachments32npx -y tsx <baseDir>/scripts/build-batch.ts --outline outline.md --prompts prompts --output batch.json --images-dir attachments3334Options:35--outline <path> Path to outline.md36--prompts <path> Path to prompts directory37--output <path> Path to output batch.json38--images-dir <path> Directory for generated images39--refs-dir <path> Directory holding reference images, relative to batch file (default: references)40--provider <name> Provider for baoyu-image-gen batch tasks (default: replicate)41--model <id> Explicit model for baoyu-image-gen batch tasks (default: resolved by baoyu-image-gen config/env)42--ar <ratio> Aspect ratio for all tasks (default: 16:9)43--quality <level> Quality for all tasks (default: 2k)44--jobs <count> Recommended worker count metadata (optional)45-h, --help Show help`);46}4748function parseArgs(argv: string[]): CliArgs {49const args: CliArgs = {50outlinePath: null,51promptsDir: null,52outputPath: null,53imagesDir: null,54refsDir: "references",55provider: "replicate",56model: null,57aspectRatio: "16:9",58quality: "2k",59jobs: null,60help: false,61};6263for (let i = 0; i < argv.length; i++) {64const current = argv[i]!;65if (current === "--outline") args.outlinePath = argv[++i] ?? null;66else if (current === "--prompts") args.promptsDir = argv[++i] ?? null;67else if (current === "--output") args.outputPath = argv[++i] ?? null;68else if (current === "--images-dir") args.imagesDir = argv[++i] ?? null;69else if (current === "--refs-dir") args.refsDir = argv[++i] ?? args.refsDir;70else if (current === "--provider") args.provider = argv[++i] ?? args.provider;71else if (current === "--model") args.model = argv[++i] ?? args.model;72else if (current === "--ar") args.aspectRatio = argv[++i] ?? args.aspectRatio;73else if (current === "--quality") args.quality = argv[++i] ?? args.quality;74else if (current === "--jobs") {75const value = argv[++i];76args.jobs = value ? parseInt(value, 10) : null;77} else if (current === "--help" || current === "-h") {78args.help = true;79}80}81return args;82}8384function parsePromptReferences(content: string): PromptReference[] {85const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*(?:\n|$)/);86if (!fmMatch) return [];87const lines = fmMatch[1]!.split(/\r?\n/);8889const refs: PromptReference[] = [];90let current: Partial<PromptReference> | null = null;91let inReferences = false;92let listIndent = 0;9394const flush = () => {95if (current?.filename) {96refs.push({97filename: current.filename,98usage: (current.usage ?? "direct") as PromptReference["usage"],99});100}101current = null;102};103104const unquote = (raw: string): string => raw.trim().replace(/^["']|["']$/g, "");105106for (const line of lines) {107if (!line.trim() || line.trim().startsWith("#")) continue;108109const keyMatch = line.match(/^(\S[^:]*):\s*(.*)$/);110if (keyMatch) {111flush();112if (keyMatch[1] === "references") {113inReferences = true;114listIndent = 0;115continue;116}117inReferences = false;118continue;119}120121if (!inReferences) continue;122123const itemMatch = line.match(/^(\s*)-\s*(.*)$/);124if (itemMatch) {125flush();126listIndent = itemMatch[1]!.length;127current = {};128const rest = itemMatch[2]!.trim();129if (rest) {130const kv = rest.match(/^(\w+)\s*:\s*(.*)$/);131if (kv && (kv[1] === "filename" || kv[1] === "usage")) {132(current as Record<string, string>)[kv[1]] = unquote(kv[2]!);133}134}135continue;136}137138const kvMatch = line.match(/^(\s+)(\w+)\s*:\s*(.*)$/);139if (kvMatch && kvMatch[1]!.length > listIndent && current) {140if (kvMatch[2] === "filename" || kvMatch[2] === "usage") {141(current as Record<string, string>)[kvMatch[2]!] = unquote(kvMatch[3]!);142}143}144}145flush();146147return refs;148}149150function parseOutline(content: string): OutlineEntry[] {151const entries: OutlineEntry[] = [];152const blocks = content.split(/^## Illustration\s+/m).slice(1);153154for (const block of blocks) {155const indexMatch = block.match(/^(\d+)/);156const filenameMatch = block.match(/\*\*Filename\*\*:\s*(.+)/);157if (indexMatch && filenameMatch) {158entries.push({159index: parseInt(indexMatch[1]!, 10),160filename: filenameMatch[1]!.trim(),161});162}163}164return entries;165}166167async function findPromptFile(promptsDir: string, entry: OutlineEntry): Promise<string | null> {168const files = await readdir(promptsDir);169const prefix = String(entry.index).padStart(2, "0");170const match = files.find((f) => f.startsWith(prefix) && f.endsWith(".md"));171return match ? path.join(promptsDir, match) : null;172}173174async function main(): Promise<void> {175const args = parseArgs(process.argv.slice(2));176if (args.help) {177printUsage();178return;179}180181if (!args.outlinePath) {182console.error("Error: --outline is required");183process.exit(1);184}185if (!args.promptsDir) {186console.error("Error: --prompts is required");187process.exit(1);188}189if (!args.outputPath) {190console.error("Error: --output is required");191process.exit(1);192}193194const outlineContent = await readFile(args.outlinePath, "utf8");195const entries = parseOutline(outlineContent);196197if (entries.length === 0) {198console.error("No illustration entries found in outline.");199process.exit(1);200}201202const tasks = [];203for (const entry of entries) {204const promptFile = await findPromptFile(args.promptsDir, entry);205if (!promptFile) {206console.error(`Warning: No prompt file found for illustration ${entry.index}, skipping.`);207continue;208}209210const imageDir = args.imagesDir ?? path.dirname(args.outputPath);211const promptContent = await readFile(promptFile, "utf8");212const refs = parsePromptReferences(promptContent)213.filter((r) => r.usage === "direct")214.map((r) => path.posix.join(args.refsDir, r.filename));215216const task: Record<string, unknown> = {217id: `illustration-${String(entry.index).padStart(2, "0")}`,218promptFiles: [promptFile],219image: path.join(imageDir, entry.filename),220provider: args.provider,221ar: args.aspectRatio,222quality: args.quality,223};224if (args.model) task.model = args.model;225if (refs.length > 0) task.ref = refs;226tasks.push(task);227}228229const output: Record<string, unknown> = { tasks };230if (args.jobs) output.jobs = args.jobs;231232await writeFile(args.outputPath, JSON.stringify(output, null, 2) + "\n");233console.log(`Batch file written: ${args.outputPath} (${tasks.length} tasks)`);234}235236main().catch((error) => {237console.error(error instanceof Error ? error.message : String(error));238process.exit(1);239});240