#!/usr/bin/env node // Prints one image-edit request per colourway. It does not call any API. // Usage: node scripts/build-input.mjs --product "sneaker" --image https://... --recolour "the three side panels and the midsole" --colour "Navy=#1B2A4A" --colour "Coral=#E0523A" [--colour ...] [--draft] [--model primary|fallback] // --recolour describes exactly which parts change. Everything else is locked. import { readFileSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' const root = join(dirname(fileURLToPath(import.meta.url)), '..') const usage = 'Usage: node scripts/build-input.mjs --product "name" --image https://... --recolour "parts that change" --colour "Name=#RRGGBB" (1 to 6 times) [--draft] [--model primary|fallback]' const allowed = new Set(['product', 'image', 'recolour', 'colour', 'draft', 'model']) const options = { colour: [] } const args = process.argv.slice(2) for (let index = 0; index < args.length; index += 1) { const key = args[index] if (!key.startsWith('--') || !allowed.has(key.slice(2))) { console.error(`Unknown argument: ${key}`) console.error(usage) process.exit(1) } if (key === '--draft') { options.draft = true; continue } const value = args[index + 1] if (value === undefined || value.startsWith('--')) { console.error(`${key} needs a value.`) console.error(usage) process.exit(1) } if (key === '--colour') options.colour.push(value) else options[key.slice(2)] = value index += 1 } const { product, image, recolour } = options if (!product || !image || !recolour || options.colour.length < 1 || options.colour.length > 6) { console.error(usage) process.exit(1) } if (!/^https:\/\//.test(image)) { console.error('The product photo must be a public https URL.') process.exit(1) } const settings = JSON.parse(readFileSync(join(root, 'assets/generation-input.json'), 'utf8')) const template = readFileSync(join(root, 'assets/prompt-template.md'), 'utf8').trim() if (options.model !== undefined && !['primary', 'fallback'].includes(options.model)) { console.error('--model must be primary or fallback.') process.exit(1) } const useFallback = options.model === 'fallback' const requests = options.colour.map((entry) => { const at = entry.lastIndexOf('=') const name = at > 0 ? entry.slice(0, at).trim() : entry.trim() const hex = at > 0 ? entry.slice(at + 1).trim() : '' if (hex && !/^#[0-9a-f]{6}$/i.test(hex)) { console.error(`"${hex}" is not a 6-digit hex code such as #1B2A4A.`) process.exit(1) } if (!hex) console.error(`Warning: "${name}" has no hex code. The model will guess the shade; write it as "${name}=#RRGGBB".`) const values = { product, parts: recolour, colour: hex ? `${name} (${hex})` : name } // One pass over the template, so text inside a value is never treated as a placeholder. const prompt = template.replace(/\{\{(\w+)\}\}/g, (match, key) => (key in values ? values[key] : match)).replace(/\s{2,}/g, ' ').trim() if (useFallback) { // fal-ai/nano-banana-pro/edit: about $0.15 per image. It takes aspect_ratio and resolution, not image_size and quality. return { name, model: settings.fallback_model, input: { prompt, image_urls: [image], aspect_ratio: settings.fallback_aspect_ratio, resolution: '1K', num_images: 1, output_format: 'jpeg' } } } return { name, model: settings.model, input: { ...settings.input, ...(options.draft ? settings.draft : {}), image_urls: [image], prompt } } }) console.log(JSON.stringify(requests, null, 2))