#!/usr/bin/env node // Prints the image-edit request for one lifestyle scene. It does not call any API. // Usage: node scripts/build-input.mjs --product "Ostra coffee bag" --image https://... --scene cafe-counter [--ratio 4:5|1:1|16:9] [--draft] [--props "..."] // --scene is a recipe id from references/scenes.json, or free text starting with "custom:". 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://... --scene [--ratio 4:5|1:1|16:9] [--draft] [--model primary|fallback] [--props "..."]' const allowed = new Set(['product', 'image', 'scene', 'ratio', 'props', 'draft', 'model']) const options = {} 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) } options[key.slice(2)] = value index += 1 } const scenes = JSON.parse(readFileSync(join(root, 'references/scenes.json'), 'utf8')) const { product, image, scene } = options if (!product || !image || !scene) { console.error(usage) console.error(`Scene ids: ${Object.keys(scenes).join(', ')}`) process.exit(1) } if (!/^https:\/\//.test(image)) { console.error('The product photo must be a public https URL.') process.exit(1) } let recipe = scenes[scene] if (!recipe && scene.startsWith('custom:')) { recipe = { scene: scene.slice(7).trim(), light: 'soft natural light from one side, consistent colour temperature across the scene.', camera: '50mm lens, shallow depth of field.' } } if (!recipe) { console.error(`Unknown scene "${scene}". Use one of: ${Object.keys(scenes).join(', ')}, or custom:.`) process.exit(1) } const settings = JSON.parse(readFileSync(join(root, 'assets/generation-input.json'), 'utf8')) const ratio = options.ratio ?? '4:5' const size = settings.ratios[ratio] if (!size) { console.error('Ratio must be 1:1, 4:5 or 16:9.') process.exit(1) } const template = readFileSync(join(root, 'assets/prompt-template.md'), 'utf8').trim() const values = { product, scene: recipe.scene, light: recipe.light, camera: recipe.camera, props: options.props ? `Props and details, kept away from the label: ${options.props}.` : '', } // One pass over the template, so text inside a value is never treated as a placeholder. const prompt = template.replace(/\{\{(\w+)\}\}/g, (match, name) => (name in values ? values[name] : match)).replace(/\s{2,}/g, ' ').trim() const useFallback = options.model === 'fallback' if (options.model !== undefined && !['primary', 'fallback'].includes(options.model)) { console.error('--model must be primary or fallback.') process.exit(1) } if (useFallback) { // fal-ai/nano-banana-pro/edit: about $0.15 per image. It takes aspect_ratio and resolution, not image_size and quality. const input = { prompt, image_urls: [image], aspect_ratio: ratio, resolution: options.draft ? '1K' : '2K', num_images: 1, output_format: 'jpeg', } console.log(JSON.stringify({ model: settings.fallback_model, input }, null, 2)) } else { const input = { ...settings.input, ...(options.draft ? { quality: settings.draft.quality } : {}), image_size: options.draft ? settings.draft.sizes[ratio] : size, image_urls: [image], prompt, } console.log(JSON.stringify({ model: settings.model, input }, null, 2)) }