chatroom-cli 1.136.0 → 1.137.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2140,7 +2140,156 @@ var require_commander = __commonJS((exports) => {
2140
2140
  exports.InvalidOptionArgumentError = InvalidArgumentError;
2141
2141
  });
2142
2142
 
2143
+ // src/commands/generate/model-capabilities.ts
2144
+ function getModelCapability(id) {
2145
+ return Object.prototype.hasOwnProperty.call(MODEL_CAPABILITIES, id) ? MODEL_CAPABILITIES[id] : undefined;
2146
+ }
2147
+ function describeKey(capability, key, source) {
2148
+ const support = capability.params[key];
2149
+ if (support === undefined) {
2150
+ return [
2151
+ {
2152
+ kind: "unknown",
2153
+ key,
2154
+ message: `not in the CLI's capability data for ${capability.id}; not checked.`
2155
+ }
2156
+ ];
2157
+ }
2158
+ if (support.support === "unsupported") {
2159
+ return [{ kind: "unsupported", key, source, message: support.reason }];
2160
+ }
2161
+ return [];
2162
+ }
2163
+ function describeInputKeys(capability, input) {
2164
+ const disclosures = [];
2165
+ for (const key of Object.keys(input)) {
2166
+ if (key === "aspect_ratio")
2167
+ continue;
2168
+ disclosures.push(...describeKey(capability, key, "input"));
2169
+ }
2170
+ return disclosures;
2171
+ }
2172
+ function describeFlagKeys(capability, flags) {
2173
+ return flags.aspectRatio === undefined ? [] : describeKey(capability, "aspect_ratio", "flag");
2174
+ }
2175
+ function describeDefaults(capability, input) {
2176
+ return Object.entries(capability.params).flatMap(([key, support]) => {
2177
+ if (support.support !== "supported" || support.default === undefined)
2178
+ return [];
2179
+ if (input[key] !== undefined)
2180
+ return [];
2181
+ return [
2182
+ {
2183
+ kind: "default",
2184
+ key,
2185
+ message: `${key} is not set; the model default ${support.default} applies. Set one explicit value for every call of a set.`
2186
+ }
2187
+ ];
2188
+ });
2189
+ }
2190
+ function describeNotes(capability) {
2191
+ return capability.notes.map((message) => ({ kind: "note", message }));
2192
+ }
2193
+ function describeConstraints(model, input, flags) {
2194
+ const capability = getModelCapability(model);
2195
+ if (capability === undefined) {
2196
+ return [
2197
+ {
2198
+ kind: "note",
2199
+ message: `The CLI has no capability data for ${model}; no constraints were checked.`
2200
+ }
2201
+ ];
2202
+ }
2203
+ return [
2204
+ ...describeInputKeys(capability, input),
2205
+ ...describeFlagKeys(capability, flags),
2206
+ ...describeDefaults(capability, input),
2207
+ ...describeNotes(capability)
2208
+ ];
2209
+ }
2210
+ var FLUX_2_FLASH_MODEL = "fal-ai/flux-2/flash", FLUX_2_FLASH_EDIT_MODEL = "fal-ai/flux-2/flash/edit", FLASH_SOURCE = "https://fal.ai/api/openapi/queue/openapi.json?endpoint_id=fal-ai/flux-2/flash (read 2026-10-24); no negative prompt: https://help.bfl.ai/articles/7734566352-does-flux-2-support-negative-prompting (read 2026-10-24)", EDIT_SOURCE = "https://fal.ai/api/openapi/queue/openapi.json?endpoint_id=fal-ai/flux-2/flash/edit (read 2026-10-24); no negative prompt: https://help.bfl.ai/articles/7734566352-does-flux-2-support-negative-prompting (read 2026-10-24)", MODEL_CAPABILITIES;
2211
+ var init_model_capabilities = __esm(() => {
2212
+ MODEL_CAPABILITIES = {
2213
+ [FLUX_2_FLASH_MODEL]: {
2214
+ id: FLUX_2_FLASH_MODEL,
2215
+ name: "FLUX 2 Flash",
2216
+ task: "text-to-image",
2217
+ params: {
2218
+ prompt: { support: "supported" },
2219
+ seed: { support: "supported" },
2220
+ output_format: { support: "supported" },
2221
+ image_size: {
2222
+ support: "supported",
2223
+ default: "landscape_4_3",
2224
+ note: "set one explicit value for every call of a set"
2225
+ },
2226
+ aspect_ratio: {
2227
+ support: "unsupported",
2228
+ reason: `FLUX 2 Flash sizes output with image_size. Pass --input '{"image_size":"landscape_4_3"}' instead.`
2229
+ },
2230
+ negative_prompt: {
2231
+ support: "unsupported",
2232
+ reason: "FLUX 2 Flash has no negative prompt; describe what you want in --prompt instead."
2233
+ },
2234
+ image_urls: {
2235
+ support: "unsupported",
2236
+ reason: "Text-to-image only; use --model fal-ai/flux-2/flash/edit with --input-image."
2237
+ }
2238
+ },
2239
+ notes: [
2240
+ "FLUX 2 Flash has no camera or perspective parameter; perspective comes only from the prompt.",
2241
+ "Same-seed reproducibility is not documented by fal.",
2242
+ "Returns PNG without an alpha channel by default."
2243
+ ],
2244
+ source: FLASH_SOURCE
2245
+ },
2246
+ [FLUX_2_FLASH_EDIT_MODEL]: {
2247
+ id: FLUX_2_FLASH_EDIT_MODEL,
2248
+ name: "FLUX 2 Flash (edit)",
2249
+ task: "image-edit",
2250
+ params: {
2251
+ prompt: { support: "supported" },
2252
+ seed: { support: "supported" },
2253
+ image_urls: {
2254
+ support: "supported",
2255
+ note: "set by --input-image; the CLI sends one image"
2256
+ },
2257
+ image_size: {
2258
+ support: "supported",
2259
+ default: "square_hd",
2260
+ note: "set one explicit value for every call of a set"
2261
+ },
2262
+ aspect_ratio: {
2263
+ support: "unsupported",
2264
+ reason: `FLUX 2 Flash (edit) sizes output with image_size. Pass --input '{"image_size":"square_hd"}' instead.`
2265
+ },
2266
+ negative_prompt: {
2267
+ support: "unsupported",
2268
+ reason: "FLUX 2 Flash has no negative prompt; describe what you want in --prompt instead."
2269
+ }
2270
+ },
2271
+ notes: [
2272
+ "FLUX 2 Flash has no camera or perspective parameter; perspective comes only from the prompt.",
2273
+ "Not documented to carry a reference image's perspective over to new content; it may return a modified copy of the input.",
2274
+ "Same-seed reproducibility is not documented by fal."
2275
+ ],
2276
+ source: EDIT_SOURCE
2277
+ }
2278
+ };
2279
+ });
2280
+
2143
2281
  // src/commands/generate/image.ts
2282
+ var exports_image = {};
2283
+ __export(exports_image, {
2284
+ planGenerateImage: () => planGenerateImage,
2285
+ parseInputJson: () => parseInputJson,
2286
+ generateImage: () => generateImage,
2287
+ formatGenerateImagePlan: () => formatGenerateImagePlan,
2288
+ GenerateImageError: () => GenerateImageError,
2289
+ GENERATE_IMAGE_PLATFORMS: () => GENERATE_IMAGE_PLATFORMS,
2290
+ FAL_API_KEY_MISSING_MESSAGE: () => FAL_API_KEY_MISSING_MESSAGE,
2291
+ DEFAULT_GENERATE_IMAGE_PLATFORM: () => DEFAULT_GENERATE_IMAGE_PLATFORM
2292
+ });
2144
2293
  import { createHash } from "node:crypto";
2145
2294
  import { extname, resolve } from "node:path";
2146
2295
  function resolvePlatform(platform) {
@@ -2163,6 +2312,14 @@ function requireNonEmpty(value, message) {
2163
2312
  }
2164
2313
  return trimmed;
2165
2314
  }
2315
+ function inputImageContentType(path) {
2316
+ const extension = extname(path).toLowerCase();
2317
+ const contentType = INPUT_IMAGE_CONTENT_TYPES[extension];
2318
+ if (contentType === undefined) {
2319
+ throw new GenerateImageError(`Unsupported --input-image extension ${extension || "(none)"}. Use one of: ${Object.keys(INPUT_IMAGE_CONTENT_TYPES).join(", ")}.`);
2320
+ }
2321
+ return contentType;
2322
+ }
2166
2323
  function validateInputObject(input) {
2167
2324
  if ("sync_mode" in input && input.sync_mode !== undefined) {
2168
2325
  throw new GenerateImageError('--input must not contain "sync_mode": it returns data URIs that this command cannot download. Omit it.');
@@ -2176,6 +2333,9 @@ function validateInputObject(input) {
2176
2333
  }
2177
2334
  }
2178
2335
  }
2336
+ function isPlainObject(value) {
2337
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2338
+ }
2179
2339
  function parseInputJson(text) {
2180
2340
  let parsed;
2181
2341
  try {
@@ -2183,28 +2343,30 @@ function parseInputJson(text) {
2183
2343
  } catch {
2184
2344
  throw new GenerateImageError("--input must be valid JSON.");
2185
2345
  }
2186
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2346
+ if (!isPlainObject(parsed)) {
2187
2347
  throw new GenerateImageError("--input must be a JSON object.");
2188
2348
  }
2189
- const input = parsed;
2190
- validateInputObject(input);
2191
- return input;
2349
+ validateInputObject(parsed);
2350
+ return parsed;
2192
2351
  }
2193
- function buildInput(prompt, opts) {
2194
- if (opts.input !== undefined) {
2195
- if (typeof opts.input !== "object" || opts.input === null || Array.isArray(opts.input)) {
2196
- throw new GenerateImageError("--input must be a JSON object.");
2197
- }
2198
- validateInputObject(opts.input);
2199
- }
2352
+ function buildInput(prompt, opts, imageUrl) {
2200
2353
  return {
2201
2354
  prompt,
2202
2355
  ...opts.input ?? {},
2203
2356
  ...opts.aspectRatio !== undefined ? { aspect_ratio: opts.aspectRatio } : {},
2204
2357
  ...opts.outputFormat !== undefined ? { output_format: opts.outputFormat } : {},
2205
- ...opts.seed !== undefined ? { seed: opts.seed } : {}
2358
+ ...opts.seed !== undefined ? { seed: opts.seed } : {},
2359
+ ...imageUrl !== undefined ? { image_urls: [imageUrl] } : {}
2206
2360
  };
2207
2361
  }
2362
+ function validateInputOption(opts) {
2363
+ if (opts.input === undefined)
2364
+ return;
2365
+ if (!isPlainObject(opts.input)) {
2366
+ throw new GenerateImageError("--input must be a JSON object.");
2367
+ }
2368
+ validateInputObject(opts.input);
2369
+ }
2208
2370
  function requireImage(images, model) {
2209
2371
  const image = images[0];
2210
2372
  if (!image?.url) {
@@ -2266,6 +2428,7 @@ function buildManifest(args) {
2266
2428
  sdk: `@fal-ai/client@${args.sdkVersion}`,
2267
2429
  model: args.model,
2268
2430
  input: args.input,
2431
+ inputImage: args.inputImage,
2269
2432
  requestId: args.requestId ?? null,
2270
2433
  seed: resolveSeed(args.resultSeed, args.requestedSeed),
2271
2434
  output: {
@@ -2278,21 +2441,118 @@ function buildManifest(args) {
2278
2441
  licensing: LICENSING
2279
2442
  };
2280
2443
  }
2281
- async function generateImage(opts, deps) {
2444
+ function requireTaskPairing(capability, model, inputImage) {
2445
+ if (capability.task === "image-edit" && inputImage === undefined) {
2446
+ throw new GenerateImageError(`${model} edits an image; pass --input-image <path-or-url>.`);
2447
+ }
2448
+ if (capability.task === "text-to-image" && inputImage !== undefined) {
2449
+ throw new GenerateImageError(`${model} does not take an input image; use --model ${FLUX_2_FLASH_EDIT_MODEL} with --input-image.`);
2450
+ }
2451
+ }
2452
+ function requireAspectRatioSupport(capability, model, aspectRatio) {
2453
+ if (aspectRatio === undefined)
2454
+ return;
2455
+ const support = capability.params.aspect_ratio;
2456
+ if (support?.support !== "unsupported")
2457
+ return;
2458
+ throw new GenerateImageError(`--aspect-ratio is not supported by ${model}. ${support.reason}`);
2459
+ }
2460
+ function requireModelPairing(model, inputImage, aspectRatio) {
2461
+ const capability = getModelCapability(model);
2462
+ if (capability === undefined)
2463
+ return;
2464
+ requireTaskPairing(capability, model, inputImage);
2465
+ requireAspectRatioSupport(capability, model, aspectRatio);
2466
+ }
2467
+ function resolveInputImageOption(opts) {
2468
+ return opts.inputImage === undefined ? undefined : requireNonEmpty(opts.inputImage, "--input-image must not be empty.");
2469
+ }
2470
+ async function validateInputImage(deps, inputImage) {
2471
+ if (inputImage === undefined)
2472
+ return;
2473
+ if (HTTP_URL_PATTERN.test(inputImage))
2474
+ return { kind: "url", url: inputImage };
2475
+ const localPath = resolve(inputImage);
2476
+ const contentType = inputImageContentType(localPath);
2477
+ const status = await deps.pathStatus(localPath);
2478
+ if (status !== "file") {
2479
+ throw new GenerateImageError(`--input-image is not a file: ${localPath}`);
2480
+ }
2481
+ return { kind: "file", path: localPath, contentType };
2482
+ }
2483
+ function planInputImageUrl(target) {
2484
+ if (target === undefined)
2485
+ return;
2486
+ return target.kind === "url" ? target.url : `<fal storage URL for ${target.path}>`;
2487
+ }
2488
+ async function uploadInputImage(deps, target) {
2489
+ if (target === undefined)
2490
+ return { url: undefined, record: null };
2491
+ if (target.kind === "url") {
2492
+ return { url: target.url, record: { source: target.url, url: target.url } };
2493
+ }
2494
+ const url = await deps.uploadFile(target.path, target.contentType);
2495
+ return { url, record: { source: target.path, url } };
2496
+ }
2497
+ async function planGenerateImage(opts, deps) {
2282
2498
  const platform = resolvePlatform(opts.platform);
2283
2499
  requireSupportedPlatform(platform);
2284
- requireFalKey(deps);
2500
+ const model = requireNonEmpty(opts.model, "--model must not be empty.");
2285
2501
  const prompt = requireNonEmpty(opts.prompt, "--prompt must not be empty.");
2286
2502
  const outPath = resolve(requireNonEmpty(opts.out, "--out must not be empty."));
2287
2503
  requireSupportedExtension(outPath);
2288
2504
  if (opts.outputFormat !== undefined) {
2289
2505
  requireFormatMatchesExtension(outPath, opts.outputFormat);
2290
2506
  }
2507
+ const inputImage = resolveInputImageOption(opts);
2508
+ requireModelPairing(model, inputImage, opts.aspectRatio);
2509
+ validateInputOption(opts);
2510
+ const inputTarget = await validateInputImage(deps, inputImage);
2291
2511
  const manifestPath = `${outPath}.json`;
2292
2512
  await requireWritableOutput(deps, outPath, manifestPath, opts.force === true);
2293
- await deps.ensureParentDir(outPath);
2294
- const model = opts.model ?? DEFAULT_IMAGE_MODEL;
2295
- const input = buildInput(prompt, opts);
2513
+ const input = buildInput(prompt, opts, planInputImageUrl(inputTarget));
2514
+ return {
2515
+ platform,
2516
+ model,
2517
+ outPath,
2518
+ manifestPath,
2519
+ input,
2520
+ inputTarget,
2521
+ disclosures: describeConstraints(model, input, { aspectRatio: opts.aspectRatio })
2522
+ };
2523
+ }
2524
+ function disclosureFlag(key) {
2525
+ return key === "aspect_ratio" ? "--aspect-ratio" : "--input";
2526
+ }
2527
+ function formatDisclosure(disclosure) {
2528
+ if (disclosure.kind === "unsupported" || disclosure.kind === "unknown") {
2529
+ return `- ${disclosure.kind}: ${disclosure.key} (${disclosureFlag(disclosure.key)}) — ${disclosure.message}`;
2530
+ }
2531
+ return `- ${disclosure.kind}: ${disclosure.message}`;
2532
+ }
2533
+ function formatInputImageLine(target) {
2534
+ if (target === undefined)
2535
+ return;
2536
+ return target.kind === "url" ? `Input image ${target.url} (passed through)` : `Input image ${target.path} (would be uploaded to fal storage, 1-day lifetime)`;
2537
+ }
2538
+ function formatGenerateImagePlan(plan) {
2539
+ const capability = getModelCapability(plan.model);
2540
+ const lines = [
2541
+ `Would generate ${plan.outPath}`,
2542
+ `Manifest ${plan.manifestPath}`,
2543
+ capability === undefined ? `Model ${plan.model} (no capability data)` : `Model ${plan.model} (${capability.name}, ${capability.task})`
2544
+ ];
2545
+ const inputLine = formatInputImageLine(plan.inputTarget);
2546
+ if (inputLine !== undefined)
2547
+ lines.push(inputLine);
2548
+ lines.push("Request:", JSON.stringify(plan.input, null, 2), "Constraints:");
2549
+ for (const disclosure of plan.disclosures)
2550
+ lines.push(formatDisclosure(disclosure));
2551
+ lines.push("No upload or fal call was made.");
2552
+ return lines.join(`
2553
+ `);
2554
+ }
2555
+ async function generateAndSave(deps, model, input, outPath) {
2296
2556
  const result = await deps.falSubscribe(model, input);
2297
2557
  if (result.data.images.length > 1) {
2298
2558
  deps.warn(`⚠️ fal returned ${result.data.images.length} images; only the first is saved.`);
@@ -2300,25 +2560,46 @@ async function generateImage(opts, deps) {
2300
2560
  const image = requireImage(result.data.images, model);
2301
2561
  const bytes = await downloadImage(deps, result.requestId, image.url);
2302
2562
  await saveImage(deps, outPath, bytes, result.requestId, image.url);
2303
- const sha256 = createHash("sha256").update(bytes).digest("hex");
2304
- await deps.writeJson(manifestPath, buildManifest({
2305
- platform,
2306
- model,
2307
- input,
2563
+ return {
2564
+ image,
2565
+ bytes,
2566
+ sha256: createHash("sha256").update(bytes).digest("hex"),
2308
2567
  requestId: result.requestId,
2309
- resultSeed: result.data.seed,
2568
+ seed: result.data.seed
2569
+ };
2570
+ }
2571
+ async function generateImage(opts, deps) {
2572
+ requireFalKey(deps);
2573
+ const plan = await planGenerateImage(opts, deps);
2574
+ for (const disclosure of plan.disclosures) {
2575
+ if (disclosure.kind === "unsupported") {
2576
+ deps.warn(`⚠️ ${disclosure.key}: ${disclosure.message}`);
2577
+ }
2578
+ }
2579
+ await deps.ensureParentDir(plan.outPath);
2580
+ const preparedInput = await uploadInputImage(deps, plan.inputTarget);
2581
+ const input = preparedInput.url === undefined ? plan.input : { ...plan.input, image_urls: [preparedInput.url] };
2582
+ const saved = await generateAndSave(deps, plan.model, input, plan.outPath);
2583
+ await deps.writeJson(plan.manifestPath, buildManifest({
2584
+ platform: plan.platform,
2585
+ model: plan.model,
2586
+ input,
2587
+ inputImage: preparedInput.record,
2588
+ requestId: saved.requestId,
2589
+ resultSeed: saved.seed,
2310
2590
  requestedSeed: opts.seed,
2311
- outPath,
2312
- sha256,
2313
- contentType: image.content_type,
2314
- byteLength: bytes.byteLength,
2591
+ outPath: plan.outPath,
2592
+ sha256: saved.sha256,
2593
+ contentType: saved.image.content_type,
2594
+ byteLength: saved.bytes.byteLength,
2315
2595
  generatedAt: deps.now().toISOString(),
2316
2596
  sdkVersion: deps.sdkVersion
2317
2597
  }));
2318
- return { outPath, manifestPath, sha256 };
2598
+ return { outPath: plan.outPath, manifestPath: plan.manifestPath, sha256: saved.sha256 };
2319
2599
  }
2320
- var DEFAULT_IMAGE_MODEL = "fal-ai/flux-2/flash", GENERATE_IMAGE_PLATFORMS, DEFAULT_GENERATE_IMAGE_PLATFORM = "fal", GenerateImageError, LICENSING = "unverified", FAL_API_KEY_MISSING_MESSAGE = "FAL_API_KEY is not set. Set FAL_API_KEY in your environment to generate images with fal. (FAL_KEY_ID/FAL_KEY_SECRET are not supported by this command.)", EXTENSION_FORMATS, UNSUPPORTED_OUTPUT_EXTENSIONS, INPUT_DEDICATED_FLAGS;
2600
+ var GENERATE_IMAGE_PLATFORMS, DEFAULT_GENERATE_IMAGE_PLATFORM = "fal", GenerateImageError, LICENSING = "unverified", FAL_API_KEY_MISSING_MESSAGE = "FAL_API_KEY is not set. Set FAL_API_KEY in your environment to generate images with fal. (FAL_KEY_ID/FAL_KEY_SECRET are not supported by this command.)", EXTENSION_FORMATS, UNSUPPORTED_OUTPUT_EXTENSIONS, INPUT_DEDICATED_FLAGS, INPUT_IMAGE_CONTENT_TYPES, HTTP_URL_PATTERN;
2321
2601
  var init_image = __esm(() => {
2602
+ init_model_capabilities();
2322
2603
  GENERATE_IMAGE_PLATFORMS = ["fal"];
2323
2604
  GenerateImageError = class GenerateImageError extends Error {
2324
2605
  constructor(message) {
@@ -2335,8 +2616,17 @@ var init_image = __esm(() => {
2335
2616
  prompt: "--prompt",
2336
2617
  seed: "--seed",
2337
2618
  output_format: "--output-format",
2338
- aspect_ratio: "--aspect-ratio"
2619
+ aspect_ratio: "--aspect-ratio",
2620
+ image_urls: "--input-image"
2621
+ };
2622
+ INPUT_IMAGE_CONTENT_TYPES = {
2623
+ ".png": "image/png",
2624
+ ".jpg": "image/jpeg",
2625
+ ".jpeg": "image/jpeg",
2626
+ ".webp": "image/webp",
2627
+ ".gif": "image/gif"
2339
2628
  };
2629
+ HTTP_URL_PATTERN = /^https?:\/\//i;
2340
2630
  });
2341
2631
 
2342
2632
  // src/commands/generate/local-image-common.ts
@@ -2350,6 +2640,13 @@ function sha256(bytes) {
2350
2640
  function errorMessage(err) {
2351
2641
  return err instanceof Error ? err.message : String(err);
2352
2642
  }
2643
+ function median(values) {
2644
+ const sorted = [...values].sort((a, b) => a - b);
2645
+ const middle = Math.floor(sorted.length / 2);
2646
+ if (sorted.length % 2 === 1)
2647
+ return sorted[middle] ?? 0;
2648
+ return Math.round(((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2);
2649
+ }
2353
2650
  function hasPngMagic(bytes) {
2354
2651
  if (bytes.length < PNG_MAGIC.length)
2355
2652
  return false;
@@ -2668,7 +2965,7 @@ var require_utils = __commonJS((exports) => {
2668
2965
  exports.isValidUrl = isValidUrl;
2669
2966
  exports.throttle = throttle;
2670
2967
  exports.isReact = isReact;
2671
- exports.isPlainObject = isPlainObject;
2968
+ exports.isPlainObject = isPlainObject2;
2672
2969
  exports.sleep = sleep;
2673
2970
  function ensureEndpointIdFormat(id) {
2674
2971
  const parts = id.split("/");
@@ -2744,7 +3041,7 @@ var require_utils = __commonJS((exports) => {
2744
3041
  }
2745
3042
  return isRunningInReact;
2746
3043
  }
2747
- function isPlainObject(value) {
3044
+ function isPlainObject2(value) {
2748
3045
  return !!value && Object.getPrototypeOf(value) === Object.prototype;
2749
3046
  }
2750
3047
  function sleep(ms) {
@@ -6495,7 +6792,7 @@ __export(exports_deps, {
6495
6792
  createFalRawSubscribe: () => createFalRawSubscribe,
6496
6793
  createDefaultGenerateImageDeps: () => createDefaultGenerateImageDeps
6497
6794
  });
6498
- import { mkdir } from "node:fs/promises";
6795
+ import { mkdir, readFile } from "node:fs/promises";
6499
6796
  import { dirname as dirname2 } from "node:path";
6500
6797
  function isRecord2(value) {
6501
6798
  return typeof value === "object" && value !== null;
@@ -6598,13 +6895,16 @@ function describeFalError(err) {
6598
6895
  return formatApiError(err);
6599
6896
  return err instanceof Error ? err.message : String(err);
6600
6897
  }
6898
+ function configureFal(env) {
6899
+ const credentials = env.FAL_API_KEY;
6900
+ if (!credentials) {
6901
+ throw new GenerateImageError(FAL_API_KEY_MISSING_MESSAGE);
6902
+ }
6903
+ import_client.fal.config({ credentials });
6904
+ }
6601
6905
  function createFalRawSubscribe(env) {
6602
6906
  return async (model, input) => {
6603
- const credentials = env.FAL_API_KEY;
6604
- if (!credentials) {
6605
- throw new GenerateImageError(FAL_API_KEY_MISSING_MESSAGE);
6606
- }
6607
- import_client.fal.config({ credentials });
6907
+ configureFal(env);
6608
6908
  try {
6609
6909
  return await import_client.fal.subscribe(model, {
6610
6910
  input,
@@ -6620,6 +6920,18 @@ function createFalSubscribe(env) {
6620
6920
  const subscribe = createFalRawSubscribe(env);
6621
6921
  return async (model, input) => validateFalResult(await subscribe(model, input));
6622
6922
  }
6923
+ function createFalUploadFile(env) {
6924
+ return async (path, contentType) => {
6925
+ configureFal(env);
6926
+ try {
6927
+ const bytes = await readFile(path);
6928
+ const blob = new Blob([new Uint8Array(bytes)], { type: contentType });
6929
+ return await import_client.fal.storage.upload(blob, { lifecycle: { expiresIn: "1d" } });
6930
+ } catch (err) {
6931
+ throw new GenerateImageError(`Failed to upload input image ${path}: ${describeFalError(err)}`);
6932
+ }
6933
+ };
6934
+ }
6623
6935
  async function ensureParentDir(path) {
6624
6936
  await mkdir(dirname2(path), { recursive: true });
6625
6937
  }
@@ -6642,6 +6954,7 @@ function createDefaultGenerateImageDeps() {
6642
6954
  return {
6643
6955
  env,
6644
6956
  falSubscribe: createFalSubscribe(env),
6957
+ uploadFile: createFalUploadFile(env),
6645
6958
  fetchBytes,
6646
6959
  writeFile: writeFileBytes,
6647
6960
  writeJson: writeJsonFile,
@@ -8552,7 +8865,7 @@ var exports_slice_deps = {};
8552
8865
  __export(exports_slice_deps, {
8553
8866
  createDefaultSliceImageDeps: () => createDefaultSliceImageDeps
8554
8867
  });
8555
- import { lstat, mkdir as mkdir2, readFile as readFile2, stat as stat3 } from "node:fs/promises";
8868
+ import { lstat, mkdir as mkdir2, readFile as readFile3, stat as stat3 } from "node:fs/promises";
8556
8869
  function decodePng(bytes) {
8557
8870
  const png = PNG.sync.read(Buffer.from(bytes));
8558
8871
  return { width: png.width, height: png.height, data: png.data };
@@ -8582,7 +8895,7 @@ async function statInput(path) {
8582
8895
  async function readJsonIfExists(path) {
8583
8896
  let text;
8584
8897
  try {
8585
- text = await readFile2(path, "utf8");
8898
+ text = await readFile3(path, "utf8");
8586
8899
  } catch (err) {
8587
8900
  if (err.code === "ENOENT") {
8588
8901
  return;
@@ -8619,7 +8932,7 @@ function createDefaultSliceImageDeps() {
8619
8932
  return {
8620
8933
  pathStatus,
8621
8934
  statInput,
8622
- readFile: readFile2,
8935
+ readFile: readFile3,
8623
8936
  readJsonIfExists,
8624
8937
  decodePng,
8625
8938
  encodePng,
@@ -27379,7 +27692,7 @@ __export(exports_util, {
27379
27692
  jsonStringifyReplacer: () => jsonStringifyReplacer,
27380
27693
  joinValues: () => joinValues,
27381
27694
  issue: () => issue,
27382
- isPlainObject: () => isPlainObject,
27695
+ isPlainObject: () => isPlainObject2,
27383
27696
  isObject: () => isObject,
27384
27697
  hexToUint8Array: () => hexToUint8Array,
27385
27698
  getSizableOrigin: () => getSizableOrigin,
@@ -27544,7 +27857,7 @@ function slugify(input) {
27544
27857
  function isObject(data) {
27545
27858
  return typeof data === "object" && data !== null && !Array.isArray(data);
27546
27859
  }
27547
- function isPlainObject(o) {
27860
+ function isPlainObject2(o) {
27548
27861
  if (isObject(o) === false)
27549
27862
  return false;
27550
27863
  const ctor = o.constructor;
@@ -27561,7 +27874,7 @@ function isPlainObject(o) {
27561
27874
  return true;
27562
27875
  }
27563
27876
  function shallowClone(o) {
27564
- if (isPlainObject(o))
27877
+ if (isPlainObject2(o))
27565
27878
  return { ...o };
27566
27879
  if (Array.isArray(o))
27567
27880
  return [...o];
@@ -27701,7 +28014,7 @@ function omit2(schema, mask) {
27701
28014
  return clone(schema, def);
27702
28015
  }
27703
28016
  function extend(schema, shape) {
27704
- if (!isPlainObject(shape)) {
28017
+ if (!isPlainObject2(shape)) {
27705
28018
  throw new Error("Invalid input to extend: expected a plain object");
27706
28019
  }
27707
28020
  const checks = schema._zod.def.checks;
@@ -27724,7 +28037,7 @@ function extend(schema, shape) {
27724
28037
  return clone(schema, def);
27725
28038
  }
27726
28039
  function safeExtend(schema, shape) {
27727
- if (!isPlainObject(shape)) {
28040
+ if (!isPlainObject2(shape)) {
27728
28041
  throw new Error("Invalid input to safeExtend: expected a plain object");
27729
28042
  }
27730
28043
  const def = mergeDefs(schema._zod.def, {
@@ -29207,7 +29520,7 @@ function mergeValues(a, b) {
29207
29520
  if (a instanceof Date && b instanceof Date && +a === +b) {
29208
29521
  return { valid: true, data: a };
29209
29522
  }
29210
- if (isPlainObject(a) && isPlainObject(b)) {
29523
+ if (isPlainObject2(a) && isPlainObject2(b)) {
29211
29524
  const bKeys = Object.keys(b);
29212
29525
  const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
29213
29526
  const newObj = { ...a, ...b };
@@ -30464,7 +30777,7 @@ var init_schemas = __esm(() => {
30464
30777
  $ZodType.init(inst, def);
30465
30778
  inst._zod.parse = (payload, ctx) => {
30466
30779
  const input = payload.value;
30467
- if (!isPlainObject(input)) {
30780
+ if (!isPlainObject2(input)) {
30468
30781
  payload.issues.push({
30469
30782
  expected: "record",
30470
30783
  code: "invalid_type",
@@ -71424,7 +71737,7 @@ var require_filesystem = __commonJS((exports, module) => {
71424
71737
  fs6.close(fd, () => {});
71425
71738
  return buffer.subarray(0, bytesRead);
71426
71739
  };
71427
- var readFile6 = (path2) => new Promise((resolve6, reject) => {
71740
+ var readFile7 = (path2) => new Promise((resolve6, reject) => {
71428
71741
  fs6.open(path2, "r", (err, fd) => {
71429
71742
  if (err) {
71430
71743
  reject(err);
@@ -71441,7 +71754,7 @@ var require_filesystem = __commonJS((exports, module) => {
71441
71754
  LDD_PATH,
71442
71755
  SELF_PATH,
71443
71756
  readFileSync: readFileSync12,
71444
- readFile: readFile6
71757
+ readFile: readFile7
71445
71758
  };
71446
71759
  });
71447
71760
 
@@ -71483,7 +71796,7 @@ var require_elf = __commonJS((exports, module) => {
71483
71796
  var require_detect_libc = __commonJS((exports, module) => {
71484
71797
  var childProcess = __require("child_process");
71485
71798
  var { isLinux, getReport } = require_process();
71486
- var { LDD_PATH, SELF_PATH, readFile: readFile6, readFileSync: readFileSync12 } = require_filesystem();
71799
+ var { LDD_PATH, SELF_PATH, readFile: readFile7, readFileSync: readFileSync12 } = require_filesystem();
71487
71800
  var { interpreterPath } = require_elf();
71488
71801
  var cachedFamilyInterpreter;
71489
71802
  var cachedFamilyFilesystem;
@@ -71563,7 +71876,7 @@ var require_detect_libc = __commonJS((exports, module) => {
71563
71876
  }
71564
71877
  cachedFamilyFilesystem = null;
71565
71878
  try {
71566
- const lddContent = await readFile6(LDD_PATH);
71879
+ const lddContent = await readFile7(LDD_PATH);
71567
71880
  cachedFamilyFilesystem = getFamilyFromLddContent(lddContent);
71568
71881
  } catch (e) {}
71569
71882
  return cachedFamilyFilesystem;
@@ -71585,7 +71898,7 @@ var require_detect_libc = __commonJS((exports, module) => {
71585
71898
  }
71586
71899
  cachedFamilyInterpreter = null;
71587
71900
  try {
71588
- const selfContent = await readFile6(SELF_PATH);
71901
+ const selfContent = await readFile7(SELF_PATH);
71589
71902
  const path2 = interpreterPath(selfContent);
71590
71903
  cachedFamilyInterpreter = familyFromInterpreterPath(path2);
71591
71904
  } catch (e) {}
@@ -71645,7 +71958,7 @@ var require_detect_libc = __commonJS((exports, module) => {
71645
71958
  }
71646
71959
  cachedVersionFilesystem = null;
71647
71960
  try {
71648
- const lddContent = await readFile6(LDD_PATH);
71961
+ const lddContent = await readFile7(LDD_PATH);
71649
71962
  const versionMatch = lddContent.match(RE_GLIBC_VERSION);
71650
71963
  if (versionMatch) {
71651
71964
  cachedVersionFilesystem = versionMatch[1];
@@ -82140,7 +82453,7 @@ var require_loadImage = __commonJS((exports, module) => {
82140
82453
  var fs8 = __require("fs");
82141
82454
  var fetch2 = global.fetch || require_lib3();
82142
82455
  var isURL = require_is_url();
82143
- var readFile6 = util.promisify(fs8.readFile);
82456
+ var readFile7 = util.promisify(fs8.readFile);
82144
82457
  module.exports = async (image) => {
82145
82458
  let data = image;
82146
82459
  if (typeof image === "undefined") {
@@ -82153,7 +82466,7 @@ var require_loadImage = __commonJS((exports, module) => {
82153
82466
  } else if (/data:image\/([a-zA-Z]*);base64,([^"]*)/.test(image)) {
82154
82467
  data = Buffer.from(image.split(",")[1], "base64");
82155
82468
  } else {
82156
- data = await readFile6(image);
82469
+ data = await readFile7(image);
82157
82470
  }
82158
82471
  } else if (Buffer.isBuffer(image)) {
82159
82472
  data = image;
@@ -82753,9 +83066,9 @@ function merge5(...objs) {
82753
83066
  }
82754
83067
  const targetKey = caseless && typeof key === "string" && findKey(result3, key) || key;
82755
83068
  const existing = hasOwnProperty(result3, targetKey) ? result3[targetKey] : undefined;
82756
- if (isPlainObject2(existing) && isPlainObject2(val)) {
83069
+ if (isPlainObject3(existing) && isPlainObject3(val)) {
82757
83070
  result3[targetKey] = merge5(existing, val);
82758
- } else if (isPlainObject2(val)) {
83071
+ } else if (isPlainObject3(val)) {
82759
83072
  result3[targetKey] = merge5({}, val);
82760
83073
  } else if (isArray2(val)) {
82761
83074
  result3[targetKey] = val.slice();
@@ -82858,7 +83171,7 @@ var toString2, getPrototypeOf, iterator, toStringTag, hasOwnProperty, isUnsafeOb
82858
83171
  }, kindOf, kindOfTest = (type) => {
82859
83172
  type = type.toLowerCase();
82860
83173
  return (thing) => kindOf(thing) === type;
82861
- }, typeOfTest = (type) => (thing) => typeof thing === type, isArray2, isUndefined2, isArrayBuffer, isString2, isFunction2, isNumber2, isObject3 = (thing) => thing !== null && typeof thing === "object", isBoolean = (thing) => thing === true || thing === false, isPlainObject2 = (val) => {
83174
+ }, typeOfTest = (type) => (thing) => typeof thing === type, isArray2, isUndefined2, isArrayBuffer, isString2, isFunction2, isNumber2, isObject3 = (thing) => thing !== null && typeof thing === "object", isBoolean = (thing) => thing === true || thing === false, isPlainObject3 = (val) => {
82862
83175
  if (!isObject3(val)) {
82863
83176
  return false;
82864
83177
  }
@@ -83134,7 +83447,7 @@ var init_utils = __esm(() => {
83134
83447
  isNumber: isNumber2,
83135
83448
  isBoolean,
83136
83449
  isObject: isObject3,
83137
- isPlainObject: isPlainObject2,
83450
+ isPlainObject: isPlainObject3,
83138
83451
  isEmptyObject,
83139
83452
  isReadableStream,
83140
83453
  isRequest,
@@ -106274,7 +106587,7 @@ var exports_parse_pdf = {};
106274
106587
  __export(exports_parse_pdf, {
106275
106588
  parsePdf: () => parsePdf
106276
106589
  });
106277
- import { access, readFile as readFile6, writeFile as writeFile7, mkdir as mkdir7, appendFile } from "node:fs/promises";
106590
+ import { access, readFile as readFile7, writeFile as writeFile7, mkdir as mkdir7, appendFile } from "node:fs/promises";
106278
106591
  import { resolve as resolve6 } from "node:path";
106279
106592
  function isUrl(input) {
106280
106593
  return input.startsWith("http://") || input.startsWith("https://");
@@ -106284,9 +106597,9 @@ function createDefaultDeps6() {
106284
106597
  fs: {
106285
106598
  access: (p) => access(p),
106286
106599
  mkdir: (p, opts) => mkdir7(p, opts).then(() => {}),
106287
- readFile: (p, enc) => readFile6(p, { encoding: enc }).then((b) => b.toString()),
106600
+ readFile: (p, enc) => readFile7(p, { encoding: enc }).then((b) => b.toString()),
106288
106601
  appendFile: (p, content) => appendFile(p, content),
106289
- readFileAsBuffer: (p) => readFile6(p),
106602
+ readFileAsBuffer: (p) => readFile7(p),
106290
106603
  writeFile: (p, content, enc) => writeFile7(p, content, enc)
106291
106604
  },
106292
106605
  parser: {
@@ -107914,7 +108227,7 @@ function formatProofOfPrinciples() {
107914
108227
  var GAME_ASSET_RULES;
107915
108228
  var init_game_asset_designer_workflow = __esm(() => {
107916
108229
  GAME_ASSET_RULES = [
107917
- "Generate only with `chatroom generate-image --platform=fal`, using the CLI default model `fal-ai/flux-2/flash` unless the brief names one.",
108230
+ "Generate only with `chatroom generate-image --platform=fal --model <Model>`, where <Model> is the brief's Model: `fal-ai/flux-2/flash` (FLUX 2 Flash), or `fal-ai/flux-2/flash/edit` with --input-image for reference edits.",
107918
108231
  "Process images only with `chatroom slice-image` and `chatroom remove-image-background`; no other tool, script, or remote service.",
107919
108232
  "A sheet is at most the brief's `Sheet grid` (default 2x2); a larger grid needs a user request quoted in the brief.",
107920
108233
  "Names come from looking at each tile, never from cell order or prompt order.",
@@ -107928,14 +108241,15 @@ var init_game_asset_designer_workflow = __esm(() => {
107928
108241
  "Never switch generators.",
107929
108242
  "Describe generated output as production-ready only when every checklist item for it passes with `measured` or `inspected` evidence.",
107930
108243
  "Commit only when the brief names the paths and the commit message.",
107931
- "Generate only when the brief names an output location and allowed tools."
108244
+ "Generate only when the brief names an output location and allowed tools.",
108245
+ "Before the first real call of each command shape (sheet, per-asset, or reference edit), run the same command with --dry-run and apply every constraint it prints; take model-specific constraints only from the dry run, never from memory."
107932
108246
  ];
107933
108247
  });
107934
108248
 
107935
108249
  // ../../services/backend/prompts/teams/game-asset-designer-handoff-templates.ts
107936
108250
  function getEntryPointToGameAssetDesignerHandoffTemplate(query) {
107937
108251
  const entryPoint = query.fromRole;
107938
- return `Instructions (not part of the message): Pass the asset request as a brief the designer can act on without guessing. Provide every field below; write \`Not specified.\` for a field you cannot fill and the designer will ask or apply a stated default. Give each asset a stable ID (\`A01\`, \`A02\`…). Use \`spec only\` only when you want a specification with no files; the default is \`spec and generate\`. The \`## Allowed tools\` value is fixed: \`chatroom generate-image --platform=fal (default model), chatroom slice-image, chatroom remove-image-background\`. Do not name any other generator.
108252
+ return `Instructions (not part of the message): Pass the asset request as a brief the designer can act on without guessing. Provide every field below; write \`Not specified.\` for a field you cannot fill and the designer will ask or apply a stated default. Give each asset a stable ID (\`A01\`, \`A02\`…). Use \`spec only\` only when you want a specification with no files; the default is \`spec and generate\`. The \`## Allowed tools\` value is fixed: \`chatroom generate-image --platform=fal --model <Model> (dry run first), chatroom slice-image, chatroom remove-image-background\`. Do not name any other generator.
107939
108253
 
107940
108254
  ## Asset request
107941
108255
  <one to three sentences: what the assets are for and the user-visible goal>
@@ -107952,11 +108266,14 @@ function getEntryPointToGameAssetDesignerHandoffTemplate(query) {
107952
108266
  ## Style reference
107953
108267
  <repository paths, URLs, or a written style sheet; or \`Not specified.\`>
107954
108268
 
108269
+ ## Perspective
108270
+ <one camera perspective shared by every asset in the set, for example \`orthographic front view\`, \`orthographic isometric\`, \`dimetric 2:1\`, or \`side view\`; optionally \`reference: <repository path>\` of an existing asset to match (requires \`## Model\` = \`fal-ai/flux-2/flash/edit\`); a reference makes every asset an individual edit, with no sheets; \`Not applicable.\` for 3D models and audio; or \`Not specified.\`>
108271
+
107955
108272
  ## Model
107956
- <the CLI default \`fal-ai/flux-2/flash\`>
108273
+ <\`fal-ai/flux-2/flash\` (FLUX 2 Flash), passed as --model; \`fal-ai/flux-2/flash/edit\` only when \`## Perspective\` names a reference asset>
107957
108274
 
107958
108275
  ## Model parameters
107959
- <a JSON object as text passed to \`generate-image --input\`; \`Not specified.\` if none>
108276
+ <a JSON object as text passed to \`generate-image --input\`; one object for every call of a set; its keys must pass the model's --dry-run check; \`Not specified.\` if none>
107960
108277
 
107961
108278
  ## Spec consumer
107962
108279
  <who integrates these assets from the spec: \`builder\` (application code) or \`user\`; asset files themselves are generated only by \`game-asset-designer\`>
@@ -107968,7 +108285,7 @@ function getEntryPointToGameAssetDesignerHandoffTemplate(query) {
107968
108285
  <\`spec and generate\` (default when you name an output location and allowed tools) or \`spec only\`>
107969
108286
 
107970
108287
  ## Allowed tools
107971
- \`chatroom generate-image --platform=fal (default model), chatroom slice-image, chatroom remove-image-background\`
108288
+ \`chatroom generate-image --platform=fal --model <Model> (dry run first), chatroom slice-image, chatroom remove-image-background\`
107972
108289
 
107973
108290
  ## Allowed edit paths
107974
108291
  <repository-relative paths the designer may edit; \`None.\` for none>
@@ -108028,7 +108345,7 @@ ${PROOF_OF_PRINCIPLES_REPORT}
108028
108345
  <each assumption you made and why; each deviation from the planner's target or budgets; or \`None.\`>
108029
108346
 
108030
108347
  ## Asset manifest
108031
- <one entry per asset, keyed by the planner's stable ID (\`A01\`…): name, category, file format, path (or \`Specified only.\`), dimensions/units/scale, pivot, naming, LOD list with triangle budgets, texture set with resolution, color space and channel packing, atlas/pixel grid/frame data, audio format and loudness target, import settings, dependencies and variants; for each budget, the acceptance check (exact command and expected value); for each generated file, the generator command line (\`chatroom generate-image\` with its \`--platform\`, \`--model\`, and \`--seed\` values), the sheet and tile manifests, and the isolated asset manifest path>
108348
+ <one entry per asset, keyed by the planner's stable ID (\`A01\`…): name, category, file format, path (or \`Specified only.\`), dimensions/units/scale, pivot, naming, LOD list with triangle budgets, texture set with resolution, color space and channel packing, atlas/pixel grid/frame data, audio format and loudness target, import settings, dependencies and variants; for each budget, the acceptance check (exact command and expected value); for each generated file, the generator command line (\`chatroom generate-image\` with its \`--platform\`, \`--model\`, \`--seed\`, \`--input\`, and \`--input-image\` values), the sheet and tile manifests, and the isolated asset manifest path>
108032
108349
 
108033
108350
  ## Quality checklist results
108034
108351
  <per asset: each applicable checklist item as \`pass\`, \`fail\`, \`n/a\`, or \`not checked\`, with its evidence label and, for \`measured\`, the command and value; write exactly \`None failed.\` after the list if no item failed>
@@ -109980,7 +110297,7 @@ P1 Recover the brief. IF engine, tier, style, or budgets are missing and the rep
109980
110297
  P2 Inspect assets, names, folders, engine config; cite paths.
109981
110298
  P3 Classify each asset; one manifest entry per ID (\`A01\`…).
109982
110299
  P4 IF \`Deliverable mode\` = \`spec only\` OR \`Output location\`/\`Allowed tools\` missing/ambiguous THEN no files, deliver \`Specified only.\`, GOTO P10.
109983
- P5 Count N. IF N=1 or styles differ THEN per-asset \`generate-image\`, GOTO P8. ELSE use ceil(N / (C×R)) sheets of ≤(C×R) items per \`Sheet grid\` (2x2 default). Prompt: one item per cell, CxR grid, no lines/borders/frames/duplicates. Generate \`generate-image --platform=fal --prompt "<prompt>" --out <Output location>/_work/<sheet>.png\`.
110300
+ P5 Count N. IF N=1 or styles differ THEN per-asset \`generate-image\`, GOTO P8. ELSE use ceil(N / (C×R)) sheets of ≤(C×R) items per \`Sheet grid\` (2x2 default). Prompt: one item per cell, CxR grid, no lines/borders/frames/duplicates. Generate \`generate-image --platform=fal --model <Model> --prompt "<prompt>" --out <Output location>/_work/<sheet>.png\`.
109984
110301
  P6 \`chatroom slice-image --in <sheet> --out-dir <Output location>/_work/tiles --cols C --rows R\`. IF refused regenerate once; IF still refused one \`generate-image\` per asset (P5).
109985
110302
  P7 View each tile. IF any tile has 0/2+ items, a cut item, or count differs from the brief's requested count (extra cells OK) THEN regenerate once; IF still bad THEN one \`generate-image\` per asset (P5). Name from sight.
109986
110303
  P8 IF \`remove-image-background --help\` succeeds THEN run \`remove-image-background --in-dir <tiles> --out-dir <Output location> --map <stem>=<name>,... --trim\` (single: \`--in <image> --out <Output location>/<name>.png --trim\`); view results. IF warnings/halo THEN rerun with \`--tolerance\`/\`--force\` max twice, ELSE accept. ELSE rerun \`slice-image --names\`; record trimming not delivered as a failed item/open question.
@@ -110019,11 +110336,20 @@ P11 Hand off to \`{ENTRY_POINT_ROLE}\`, then stop.
110019
110336
  - Animation: uniform frame size per clip, frame count, frame rate, loop flag, and pivot per clip.
110020
110337
  - Atlas: padding of at least the engine default (Unity default 4 px), atlas size within the platform max, filter mode Point for pixel art and no mip maps.
110021
110338
  - Export PNG (lossless with alpha); name states size and frame count where relevant.
110339
+ - Camera perspective matches the perspective chosen for the set (the brief's \`Perspective\`, or the one recorded under assumptions) and every other asset in the set (compare them side by side); \`n/a\` for a single asset.
110022
110340
 
110023
110341
  **Audio**
110024
110342
  - Loudness measured against the platform target stated in the request (for example console -24 LUFS, mobile -18 LUFS, true peak at or below -1 dBTP); no clipping. Measure with a tool: \`ffmpeg -i <file> -af ebur128=peak=true -f null -\` for integrated loudness and true peak; \`ffprobe <file>\` for sample rate, bit depth and channels; for glTF, \`gltf-transform inspect <file>\` (quote the command's actual output). Cite the platform source. If the request does not state the target, mark the loudness target \`unverified\` and name the source. Mobile targets vary (-18 vs -16 LUFS).
110025
110343
  - Sample rate, bit depth, channel count, loop points, and format stated; short noisy SFX use PCM or ADPCM, long audio uses compressed or streamed formats, per the engine's guidance.
110026
110344
 
110345
+ ### Perspective consistency (generated 2D sets)
110346
+ Assets used together must share one camera perspective. The model's constraints (unsupported parameters, size defaults, what it does not guarantee) come only from \`generate-image --dry-run\` (R16); apply them, and never add a parameter the dry run reports as unsupported.
110347
+ - Repeat the perspective term from the brief's \`Perspective\` word for word in every prompt of the set (never the \`reference:\` path), and add the same flattening terms to every prompt: \`flat 2D game asset, orthographic, centered, plain background, even lighting\`. Suggested perspective terms (\`unverified\`): side view, front view, orthographic top-down, orthographic isometric, dimetric 2:1, three-quarter view. If the field names only a reference, describe that reference's view in words and record the description under assumptions. If \`Perspective\` is \`Not specified.\`, pick one, record it under assumptions, and use it for the whole set. If the \`Style reference\` asks for a lighting style these terms contradict (for example dramatic lighting), keep the perspective term and drop only the conflicting flattening term.
110348
+ - Use one \`--input\` JSON (from \`Model parameters\`) on every call of the set; if the dry run reports a size parameter as defaulted, set it once in that JSON and reuse it. Add \`--input '<JSON>'\` and the seed to every P5 command.
110349
+ - Use one shared \`--seed\` for the first call of each item or sheet. For every regeneration (P6, P7, or a perspective mismatch), use a new seed and record it (P9). A shared seed does not keep perspective across different prompts.
110350
+ - Pass \`--input-image <path>\` with \`--model fal-ai/flux-2/flash/edit\` only when the brief's \`Model\` is \`fal-ai/flux-2/flash/edit\` and \`Perspective\` names a reference path. In this mode generate every asset individually (the P5 per-asset path, one \`--input-image\` per asset); never use a sheet.
110351
+ - Inspect the set side by side. A mismatched item is regenerated individually (the P5 per-asset path, with a new seed), at most twice per item, separately from the sheet regenerations of P6 and P7. If it still differs, mark the perspective checklist item \`fail\` with \`inspected\` evidence.
110352
+
110027
110353
  ### Scope boundary
110028
110354
  You may specify, name, organize, generate, validate, and integrate assets within the paths the brief allows. You must not claim fidelity you did not establish. If you cannot open or measure a file, write \`unverified\`. Do not present alternative art directions beyond what the request needs; recommend one.
110029
110355
 
@@ -118959,7 +119285,7 @@ function parseYaml(text) {
118959
119285
  }
118960
119286
  }
118961
119287
  function validateFields(data, body) {
118962
- if (!isPlainObject3(data))
119288
+ if (!isPlainObject4(data))
118963
119289
  return fail6("invalid-frontmatter", "frontmatter is not a mapping");
118964
119290
  const name = data.name;
118965
119291
  if (!isValidName(name))
@@ -118969,7 +119295,7 @@ function validateFields(data, body) {
118969
119295
  return fail6("invalid-description");
118970
119296
  return { ok: true, name, description, body };
118971
119297
  }
118972
- function isPlainObject3(value2) {
119298
+ function isPlainObject4(value2) {
118973
119299
  return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
118974
119300
  }
118975
119301
  function isValidName(value2) {
@@ -118989,7 +119315,7 @@ var init_parse_skill_file = __esm(() => {
118989
119315
  });
118990
119316
 
118991
119317
  // src/infrastructure/external-skills/discover-external-skills.ts
118992
- import { lstat as lstat2, readdir as readdir2, readFile as readFile8, realpath, stat as stat6 } from "node:fs/promises";
119318
+ import { lstat as lstat2, readdir as readdir2, readFile as readFile9, realpath, stat as stat6 } from "node:fs/promises";
118993
119319
  import os2 from "node:os";
118994
119320
  import { join as join20 } from "node:path";
118995
119321
  function errorCode(err) {
@@ -119097,7 +119423,7 @@ async function isSymlink2(path3) {
119097
119423
  }
119098
119424
  async function readSkillFile(sourcePath, issues) {
119099
119425
  try {
119100
- return await readFile8(join20(sourcePath, SKILL_FILE_NAME), "utf8");
119426
+ return await readFile9(join20(sourcePath, SKILL_FILE_NAME), "utf8");
119101
119427
  } catch (err) {
119102
119428
  issues.push(issue2(sourcePath, "unreadable", errorCode(err)));
119103
119429
  return;
@@ -121588,7 +121914,7 @@ var init_orphan_tracker = __esm(() => {
121588
121914
  });
121589
121915
 
121590
121916
  // src/daemon/entry/handlers/process/output-store.ts
121591
- import { appendFile as appendFile2, mkdir as mkdir10, readFile as readFile9, rm as rm2 } from "node:fs/promises";
121917
+ import { appendFile as appendFile2, mkdir as mkdir10, readFile as readFile10, rm as rm2 } from "node:fs/promises";
121592
121918
  import { tmpdir } from "node:os";
121593
121919
  import { join as join23 } from "node:path";
121594
121920
 
@@ -121621,7 +121947,7 @@ class TempFileOutputStore {
121621
121947
  async getLastNLines(maxLines) {
121622
121948
  let source;
121623
121949
  try {
121624
- source = await readFile9(this.state.filePath, "utf-8");
121950
+ source = await readFile10(this.state.filePath, "utf-8");
121625
121951
  } catch {
121626
121952
  source = this.state.inMemory;
121627
121953
  }
@@ -121638,7 +121964,7 @@ class TempFileOutputStore {
121638
121964
  }
121639
121965
  async getFullOutput() {
121640
121966
  try {
121641
- return await readFile9(this.state.filePath, "utf-8");
121967
+ return await readFile10(this.state.filePath, "utf-8");
121642
121968
  } catch {
121643
121969
  return this.state.inMemory;
121644
121970
  }
@@ -123268,7 +123594,7 @@ var init_workspace_visibility_policy = __esm(() => {
123268
123594
 
123269
123595
  // src/infrastructure/services/workspace/workspace-ignore.ts
123270
123596
  import { promises as fsPromises } from "node:fs";
123271
- import { readFile as readFile10 } from "node:fs/promises";
123597
+ import { readFile as readFile11 } from "node:fs/promises";
123272
123598
  import { homedir as homedir11 } from "node:os";
123273
123599
  import path3 from "node:path";
123274
123600
  function normalizeRelativePath(relativePath) {
@@ -123333,7 +123659,7 @@ async function loadGitExtraExcludeRuleSets(rootDir) {
123333
123659
  }
123334
123660
  async function readIgnoreFile(filePath) {
123335
123661
  try {
123336
- return await readFile10(filePath, "utf-8");
123662
+ return await readFile11(filePath, "utf-8");
123337
123663
  } catch {
123338
123664
  return null;
123339
123665
  }
@@ -127928,7 +128254,7 @@ function parseMakefileTargets(content) {
127928
128254
  var MAX_NAME_LENGTH2 = 256;
127929
128255
 
127930
128256
  // src/infrastructure/services/workspace/workspace-resolver.ts
127931
- import { readFile as readFile12, readdir as readdir5, stat as stat10 } from "node:fs/promises";
128257
+ import { readFile as readFile13, readdir as readdir5, stat as stat10 } from "node:fs/promises";
127932
128258
  import { join as join28, basename as basename7, resolve as resolve10 } from "node:path";
127933
128259
  async function resolveGlobPatternStar(rootDir, cleaned) {
127934
128260
  const parentDir2 = join28(rootDir, cleaned.slice(0, -2));
@@ -127969,7 +128295,7 @@ async function resolveGlobPattern(rootDir, pattern) {
127969
128295
  }
127970
128296
  async function readPackageJson(dir) {
127971
128297
  try {
127972
- const content = await readFile12(join28(dir, "package.json"), "utf-8");
128298
+ const content = await readFile13(join28(dir, "package.json"), "utf-8");
127973
128299
  const pkg = JSON.parse(content);
127974
128300
  return {
127975
128301
  name: pkg.name || basename7(dir),
@@ -127981,7 +128307,7 @@ async function readPackageJson(dir) {
127981
128307
  }
127982
128308
  async function readPnpmWorkspacePatterns(rootDir) {
127983
128309
  try {
127984
- const content = await readFile12(join28(rootDir, "pnpm-workspace.yaml"), "utf-8");
128310
+ const content = await readFile13(join28(rootDir, "pnpm-workspace.yaml"), "utf-8");
127985
128311
  const patterns = [];
127986
128312
  let inPackages = false;
127987
128313
  for (const line of content.split(`
@@ -128008,7 +128334,7 @@ async function readPnpmWorkspacePatterns(rootDir) {
128008
128334
  }
128009
128335
  async function readPackageJsonWorkspacePatterns(rootDir) {
128010
128336
  try {
128011
- const content = await readFile12(join28(rootDir, "package.json"), "utf-8");
128337
+ const content = await readFile13(join28(rootDir, "package.json"), "utf-8");
128012
128338
  const pkg = JSON.parse(content);
128013
128339
  if (!pkg.workspaces)
128014
128340
  return [];
@@ -128056,7 +128382,7 @@ async function resolveSubWorkspaces(rootDir, pm) {
128056
128382
  var init_workspace_resolver = () => {};
128057
128383
 
128058
128384
  // src/infrastructure/services/workspace/command-discovery.ts
128059
- import { access as access2, readFile as readFile13 } from "node:fs/promises";
128385
+ import { access as access2, readFile as readFile14 } from "node:fs/promises";
128060
128386
  import { join as join29, relative as relative3, basename as basename8 } from "node:path";
128061
128387
  async function detectPackageManager(workingDir) {
128062
128388
  for (const { file: file2, manager } of LOCKFILE_MAP) {
@@ -128105,7 +128431,7 @@ function isValidScriptEntry(name, script) {
128105
128431
  }
128106
128432
  async function readJsonFile(filePath, label) {
128107
128433
  try {
128108
- const content = await readFile13(filePath, "utf-8");
128434
+ const content = await readFile14(filePath, "utf-8");
128109
128435
  return parseJsonc2(content);
128110
128436
  } catch (error51) {
128111
128437
  if (error51.code !== "ENOENT") {
@@ -128182,7 +128508,7 @@ async function readMakefile(workingDir) {
128182
128508
  const commands = [];
128183
128509
  for (const fileName of ["Makefile", "makefile"]) {
128184
128510
  try {
128185
- const content = await readFile13(join29(workingDir, fileName), "utf-8");
128511
+ const content = await readFile14(join29(workingDir, fileName), "utf-8");
128186
128512
  const targets = parseMakefileTargets(content);
128187
128513
  if (targets.length === 0)
128188
128514
  continue;
@@ -134302,7 +134628,7 @@ function gunzipBase64Payload(base643, maxBytes) {
134302
134628
  var init_workspace_path_security = () => {};
134303
134629
 
134304
134630
  // src/daemon/entry/files/file-content-fulfillment.ts
134305
- import { readFile as readFile14 } from "node:fs/promises";
134631
+ import { readFile as readFile15 } from "node:fs/promises";
134306
134632
  import { gzipSync as gzipSync3 } from "node:zlib";
134307
134633
  function getErrorCause(error51) {
134308
134634
  if (typeof error51 === "object" && error51 !== null && "cause" in error51 && error51.cause !== undefined) {
@@ -134377,7 +134703,7 @@ var init_file_content_fulfillment = __esm(() => {
134377
134703
  continue;
134378
134704
  }
134379
134705
  let fileNotFound = false;
134380
- const buffer = yield* exports_Effect.catch(exports_Effect.tryPromise(() => readFile14(absolutePath)), (error51) => {
134706
+ const buffer = yield* exports_Effect.catch(exports_Effect.tryPromise(() => readFile15(absolutePath)), (error51) => {
134381
134707
  if (isENOENT(error51)) {
134382
134708
  fileNotFound = true;
134383
134709
  return exports_Effect.succeed(Buffer.alloc(0));
@@ -146086,9 +146412,10 @@ var {
146086
146412
  } = import__.default;
146087
146413
 
146088
146414
  // src/commands/generate/index.ts
146089
- import { readFile, stat as stat2 } from "node:fs/promises";
146415
+ import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
146090
146416
  init_image();
146091
146417
  init_local_image_common();
146418
+ init_model_capabilities();
146092
146419
  var OUTPUT_FORMATS = ["png", "webp"];
146093
146420
  function parsePlatform(value) {
146094
146421
  if (!GENERATE_IMAGE_PLATFORMS.includes(value)) {
@@ -146114,7 +146441,7 @@ async function readInputOption(value) {
146114
146441
  if (stats.size > MAX_SIDECAR_BYTES) {
146115
146442
  throw new Error(`--input file is ${stats.size} bytes; the maximum is ${MAX_SIDECAR_BYTES} bytes (1 MiB).`);
146116
146443
  }
146117
- return parseInputJson(await readFile(path, "utf8"));
146444
+ return parseInputJson(await readFile2(path, "utf8"));
146118
146445
  }
146119
146446
  function parseSeed(value) {
146120
146447
  const seed = Number(value);
@@ -146125,38 +146452,49 @@ function parseSeed(value) {
146125
146452
  }
146126
146453
  var HELP_AFTER = `
146127
146454
  Examples:
146128
- chatroom generate-image --platform=fal --prompt "a hero" --out hero.png
146129
- chatroom generate-image --platform=fal --prompt 'pixel-art potion, transparent-friendly flat background' --output-format png --out potion.png
146455
+ chatroom generate-image --platform=fal --model ${FLUX_2_FLASH_MODEL} --prompt "a hero" --out hero.png
146456
+ chatroom generate-image --platform=fal --model ${FLUX_2_FLASH_MODEL} --prompt 'pixel-art potion, transparent-friendly flat background' --output-format png --out potion.png
146457
+ chatroom generate-image --platform=fal --model ${FLUX_2_FLASH_EDIT_MODEL} --prompt "make the sky stormy" --input-image ./hero.png --out hero-stormy.png
146130
146458
  `;
146131
- function registerGenerateImageCommand(program2) {
146132
- program2.command("generate-image").description("Generate one image with a fal model and write a JSON manifest next to it").requiredOption("--prompt <text>", "Text prompt describing the image").requiredOption("--out <path>", "Output image path (manifest is written to <path>.json)").option("--platform <name>", "Generation platform: fal", parsePlatform, DEFAULT_GENERATE_IMAGE_PLATFORM).option("--model <id>", `fal model id. Recommended and default: ${DEFAULT_IMAGE_MODEL}`, DEFAULT_IMAGE_MODEL).option("--aspect-ratio <ratio>", `Aspect ratio, e.g. 1:1, 4:3, 16:9, 9:16 (omitted = model default). Not supported by the default model ${DEFAULT_IMAGE_MODEL}, which uses its own image size`).option("--output-format <format>", "Output format: png or webp (omitted = model default)", parseOutputFormat).option("--seed <n>", "Seed for the random number generator", parseSeed).option("--input <json|@file>", "Model-specific JSON input object merged into the fal request; @file reads JSON from a file").option("--force", "Overwrite existing output and manifest files").addHelpText("after", HELP_AFTER).action(async (options) => {
146133
- try {
146134
- const input = options.input === undefined ? undefined : await readInputOption(options.input);
146135
- const { createDefaultGenerateImageDeps: createDefaultGenerateImageDeps2 } = await Promise.resolve().then(() => (init_deps(), exports_deps));
146136
- const result = await generateImage({
146137
- prompt: options.prompt,
146138
- out: options.out,
146139
- platform: options.platform,
146140
- model: options.model,
146141
- aspectRatio: options.aspectRatio,
146142
- outputFormat: options.outputFormat,
146143
- seed: options.seed,
146144
- input,
146145
- force: options.force
146146
- }, createDefaultGenerateImageDeps2());
146147
- console.log(`Generated ${result.outPath}`);
146148
- console.log(`Manifest ${result.manifestPath}`);
146149
- } catch (err) {
146150
- console.error(`❌ ${err instanceof Error ? err.message : String(err)}`);
146151
- process.exitCode = 1;
146459
+ async function runGenerateImage(options) {
146460
+ try {
146461
+ const input = options.input === undefined ? undefined : await readInputOption(options.input);
146462
+ const { createDefaultGenerateImageDeps: createDefaultGenerateImageDeps2 } = await Promise.resolve().then(() => (init_deps(), exports_deps));
146463
+ const deps = createDefaultGenerateImageDeps2();
146464
+ const generateOptions = {
146465
+ prompt: options.prompt,
146466
+ out: options.out,
146467
+ platform: options.platform,
146468
+ model: options.model,
146469
+ aspectRatio: options.aspectRatio,
146470
+ outputFormat: options.outputFormat,
146471
+ seed: options.seed,
146472
+ input,
146473
+ inputImage: options.inputImage,
146474
+ force: options.force
146475
+ };
146476
+ if (options.dryRun === true) {
146477
+ const { formatGenerateImagePlan: formatGenerateImagePlan2, planGenerateImage: planGenerateImage2 } = await Promise.resolve().then(() => (init_image(), exports_image));
146478
+ console.log(formatGenerateImagePlan2(await planGenerateImage2(generateOptions, deps)));
146479
+ return;
146152
146480
  }
146153
- });
146481
+ const result = await generateImage(generateOptions, deps);
146482
+ console.log(`Generated ${result.outPath}`);
146483
+ console.log(`Manifest ${result.manifestPath}`);
146484
+ } catch (err) {
146485
+ console.error(`❌ ${err instanceof Error ? err.message : String(err)}`);
146486
+ process.exitCode = 1;
146487
+ }
146488
+ }
146489
+ function registerGenerateImageCommand(program2) {
146490
+ program2.command("generate-image").description("Generate one image with a fal model and write a JSON manifest next to it").requiredOption("--prompt <text>", "Text prompt describing the image").requiredOption("--out <path>", "Output image path (manifest is written to <path>.json)").option("--platform <name>", "Generation platform: fal", parsePlatform, DEFAULT_GENERATE_IMAGE_PLATFORM).requiredOption("--model <id>", `fal model id (required), for example ${FLUX_2_FLASH_MODEL}, or ${FLUX_2_FLASH_EDIT_MODEL} with --input-image`).option("--aspect-ratio <ratio>", "Aspect ratio for models that take one, e.g. 1:1, 16:9 (omitted = model default). Rejected for models the CLI knows do not support it").option("--output-format <format>", "Output format: png or webp (omitted = model default)", parseOutputFormat).option("--seed <n>", "Seed for the random number generator", parseSeed).option("--input <json|@file>", "Model-specific JSON input object merged into the fal request; @file reads JSON from a file").option("--input-image <path-or-url>", `Image to edit: a local .png, .jpg, .jpeg, .webp, or .gif (uploaded to fal) or an http(s) URL. Requires an edit model such as ${FLUX_2_FLASH_EDIT_MODEL}`).option("--force", "Overwrite existing output and manifest files").option("--dry-run", "Validate and print the request and the model constraints the CLI knows, without uploading or calling fal (FAL_API_KEY not needed)").addHelpText("after", HELP_AFTER).action(runGenerateImage);
146154
146491
  }
146155
146492
 
146156
146493
  // src/commands/generate/remove-background.ts
146157
146494
  import { basename, dirname as dirname3, extname as extname2, join as join2, resolve as resolve2 } from "node:path";
146158
146495
 
146159
146496
  // src/commands/generate/background-key.ts
146497
+ init_local_image_common();
146160
146498
  var DEFAULT_TOLERANCE = 24;
146161
146499
  var DEFAULT_MIN_AREA_PERCENT = 0.5;
146162
146500
  var BORDER_NOISE_REFUSAL_RATIO = 0.02;
@@ -146174,13 +146512,6 @@ function borderCoordinates(width, height) {
146174
146512
  }
146175
146513
  return coordinates;
146176
146514
  }
146177
- function median(values) {
146178
- const sorted = [...values].sort((a, b) => a - b);
146179
- const middle = Math.floor(sorted.length / 2);
146180
- if (sorted.length % 2 === 1)
146181
- return sorted[middle] ?? 0;
146182
- return Math.round(((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2);
146183
- }
146184
146515
  function forEachNeighbour(index, width, height, visit) {
146185
146516
  const x = index % width;
146186
146517
  const y = Math.floor(index / width);
@@ -147313,14 +147644,7 @@ function mostFrequentBorderColor(image) {
147313
147644
  }
147314
147645
  if (red.length === 0)
147315
147646
  return null;
147316
- const median2 = (values) => {
147317
- const sorted = [...values].sort((a, b) => a - b);
147318
- const middle = Math.floor(sorted.length / 2);
147319
- if (sorted.length % 2 === 1)
147320
- return sorted[middle] ?? 0;
147321
- return Math.round(((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2);
147322
- };
147323
- return [median2(red), median2(green), median2(blue)];
147647
+ return [median(red), median(green), median(blue)];
147324
147648
  }
147325
147649
  function countCellEdgeForeground(image, cell, background) {
147326
147650
  const { width, height, data } = image;
@@ -147408,7 +147732,7 @@ async function sliceImage(opts, deps) {
147408
147732
  throw new SliceImageError(`--in is ${bytes.byteLength} bytes; the maximum is ${MAX_SOURCE_BYTES} bytes (64 MiB).`);
147409
147733
  }
147410
147734
  if (!hasPngMagic(bytes)) {
147411
- throw new SliceImageError("--in is not a PNG. slice-image only reads PNG sheets; regenerate with 'chatroom generate-image --output-format png' (the default model returns PNG).");
147735
+ throw new SliceImageError("--in is not a PNG. slice-image only reads PNG sheets; regenerate with 'chatroom generate-image --model <id> --output-format png'.");
147412
147736
  }
147413
147737
  const dimensions = readPngDimensions(bytes);
147414
147738
  if (dimensions === null) {
@@ -148369,4 +148693,4 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
148369
148693
  });
148370
148694
  program2.parse();
148371
148695
 
148372
- //# debugId=CBBD6308FFB368C664756E2164756E21
148696
+ //# debugId=0C6C6A8595C191FE64756E2164756E21