#!/usr/bin/env node // Prints the text-to-image request for a synthetic model's casting portrait, used when there is no consented // photo of a real person. It does not call any API. // Usage: // node scripts/build-casting-input.mjs --brief "adult woman in her early thirties, short curly black hair, warm brown skin" [--out casting.json] import { readFileSync, writeFileSync } 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-casting-input.mjs --brief "adult ..., hair ..., skin ..." [--out casting.json]' const VALUE_FLAGS = new Set(['brief', 'out']) const fail = (message) => { console.error(`${message}\n${usage}`); process.exit(1) } const options = {} const args = process.argv.slice(2) for (let i = 0; i < args.length; i += 1) { const arg = args[i] if (!arg.startsWith('--')) fail(`Unexpected argument "${arg}".`) const key = arg.slice(2) if (!VALUE_FLAGS.has(key)) fail(`Unknown flag --${key}.`) const value = args[i + 1] if (value === undefined || value.startsWith('--')) fail(`--${key} needs a value.`) options[key] = value i += 1 } if (!options.brief) fail('--brief is required.') const brief = options.brief.trim() const adultRule = 'The brief must describe an adult: include the word "adult" and an age of 18 or over, e.g. "adult man in his late twenties".' if (!/\badult\b/i.test(brief)) fail(adultRule) // Refuse anything that reads as a minor, whatever else the brief says. if (/\b(teen|teens|teenage|teenager|child|children|kid|kids|minor|minors|schoolgirl|schoolboy|youngster|juvenile|underage|preteen|tween)\b/i.test(brief)) fail(`Refused: the brief mentions a minor. ${adultRule}`) const ages = [...brief.matchAll(/\b(\d{1,3})\s*(?:-|\s)?\s*(?:years?|yrs?|y\.?o\.?)\b|\b(?:aged?|age)\s*(\d{1,3})\b/gi)].map((m) => Number(m[1] ?? m[2])) if (ages.some((age) => age < 18)) fail(`Refused: the brief gives an age under 18. ${adultRule}`) // Ages written as words ("sixteen", "twelve-year-old") count too. if (/\b(thirteen|fourteen|fifteen|sixteen|seventeen)\b|\b(one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)[\s-]*(?:years?|yrs?)\b/i.test(brief)) fail(`Refused: the brief gives an age under 18. ${adultRule}`) const decades = /\b(twenties|thirties|forties|fifties|sixties|seventies|eighties|20s|30s|40s|50s|60s|70s|80s)\b/i.test(brief) if (/\b(girl|boy|girls|boys)\b/i.test(brief) && !(ages.some((age) => age >= 18) || decades)) fail(`Refused: "girl" or "boy" without an adult age. Say e.g. "adult woman in her twenties". ${adultRule}`) const casting = JSON.parse(readFileSync(join(root, 'assets/generation-input.json'), 'utf8')).casting const template = readFileSync(join(root, 'assets/casting-template.md'), 'utf8').match(/## Template\s+([\s\S]*?)(?=\n## |$)/)?.[1]?.trim() if (!template) fail('assets/casting-template.md is missing its "## Template" section.') // A replacer function keeps any "$" in the brief literal. const prompt = template.replace('{{brief}}', () => brief) const output = JSON.stringify({ model: casting.model, input: { ...casting.input, prompt } }, null, 2) if (options.out) { writeFileSync(options.out, output) console.log(`Wrote the casting request to ${options.out}`) } else console.log(output)