#!/usr/bin/env node // Prints the five image-edit requests (hook, 3 benefits, call to action) for one carousel. It calls no API. // Usage: node scripts/build-input.mjs --image https://... --product "kraft coffee pouch, black label reading OSTRA" \ // (--preset coffee|skincare|fashion|tech|food|supplements | --look "backdrop, light and surface" --props "scene 1|scene 2|scene 3") [--format 4:5|9:16] // --props takes exactly three scenes separated by | (one per benefit slide). A preset supplies both; --look or --props override it. import { readFileSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' function parseArgs(argv, known) { const out = {} for (let i = 0; i < argv.length; i += 1) { const k = argv[i] if (!k.startsWith('--')) fail(`Unexpected argument '${k}'.`) const name = k.slice(2) if (!known.includes(name)) fail(`Unknown flag --${name}. Known: ${known.map((x) => '--' + x).join(', ')}`) const v = argv[i + 1] if (v === undefined || v.startsWith('--')) fail(`Flag --${name} needs a value.`) out[name] = v i += 1 } return out } function fail(message) { console.error(message); process.exit(1) } function checkImage(value) { if (!/^https:\/\//.test(value) && !/^data:image\//.test(value)) fail('--image must be a public https URL or an image data URI.') } const root = join(dirname(fileURLToPath(import.meta.url)), '..') const o = parseArgs(process.argv.slice(2), ['image', 'product', 'preset', 'look', 'props', 'format']) const usage = 'Usage: node scripts/build-input.mjs --image https://... --product "what it is, label text" (--preset coffee|skincare|fashion|tech|food|supplements | --look "backdrop, light, surface" --props "scene 1|scene 2|scene 3") [--format 4:5|9:16]' if (!o.image || !o.product) fail(usage) checkImage(o.image) const settings = JSON.parse(readFileSync(join(root, 'assets/generation-input.json'), 'utf8')) const format = o.format ?? '4:5' const size = settings.sizes[format] if (!size) fail('--format must be 4:5 or 9:16') if (o.preset && !settings.presets[o.preset]) fail('--preset must be one of: ' + Object.keys(settings.presets).join(', ')) const preset = o.preset ? settings.presets[o.preset] : undefined const look = o.look ?? preset?.look if (!look || (!o.props && !preset)) fail('Give --preset, or both --look and --props, so the art direction is yours.\n' + usage) const props = (o.props ?? preset.props.join('|')).split('|').map((s) => s.trim()).filter(Boolean) if (props.length !== 3) fail('--props needs exactly three scenes separated by |') const template = readFileSync(join(root, 'assets/prompt-template.md'), 'utf8').trim() const space = settings.empty_space[format] const plan = [ ['hook', settings.layouts.hook.replace('{{space}}', () => space)], ['benefit-1', settings.layouts.benefit.replace('{{props}}', () => props[0]).replace('{{space}}', () => space)], ['benefit-2', settings.layouts.benefit.replace('{{props}}', () => props[1]).replace('{{space}}', () => space)], ['benefit-3', settings.layouts.benefit.replace('{{props}}', () => props[2]).replace('{{space}}', () => space)], ['cta', settings.layouts.cta.replace('{{space}}', () => space)], ] const requests = plan.map(([slide, layout]) => ({ slide, model: settings.model, input: { ...settings.input, image_size: size, image_urls: [o.image], prompt: template .replace('{{product}}', () => o.product) .replace('{{look}}', () => look) .replace('{{layout}}', () => layout), }, })) console.log(JSON.stringify(requests, null, 2))