@genex-ai/cli-demo 0.81.0-dev.214 → 0.84.0-dev.215

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.
Files changed (24) hide show
  1. package/dist/index.js +780 -80
  2. package/package.json +1 -1
  3. package/templates/controllers/character/first-person.ts +54 -0
  4. package/templates/controllers/character/follow-camera.ts +35 -1
  5. package/templates/skills/genex-ai-character/SKILL.md +25 -5
  6. package/templates/skills/genex-ai-hud/SKILL.md +130 -36
  7. package/templates/skills/genex-ai-hud/references/masked-fill.md +19 -13
  8. package/templates/skills/genex-ai-hud/references/stage2-prompt-template.md +2 -2
  9. package/templates/skills/genex-ai-image/SKILL.md +40 -2
  10. package/templates/skills/genex-ai-menu/SKILL.md +35 -17
  11. package/templates/skills/genex-ai-model/SKILL.md +8 -0
  12. package/templates/skills/genex-ai-music/SKILL.md +142 -0
  13. package/templates/skills/genex-ai-skybox/SKILL.md +21 -5
  14. package/templates/skills/genex-ai-video/SKILL.md +13 -7
  15. package/templates/skills/genex-ai-voice/SKILL.md +151 -0
  16. package/templates/skills/genex-game-director/SKILL.md +128 -44
  17. package/templates/skills/genex-game-director/references/design-contract.md +7 -2
  18. package/templates/skills/genex-game-director/references/routing-map.md +4 -1
  19. package/templates/skills/genex-threejs-camera-direction/SKILL.md +4 -1
  20. package/templates/skills/genex-threejs-character-controller/SKILL.md +6 -1
  21. package/templates/skills/genex-threejs-character-controller/references/wiring.md +13 -2
  22. package/templates/skills/genex-threejs-creatures/SKILL.md +193 -0
  23. package/templates/skills/genex-threejs-game-ui/SKILL.md +5 -1
  24. package/templates/skills/genex-threejs-multiplayer/SKILL.md +6 -1
package/dist/index.js CHANGED
@@ -2006,21 +2006,27 @@ var UI_NUMBER_FLAGS = {
2006
2006
  "--quantize": "quantize",
2007
2007
  "--ink-dist": "inkDist",
2008
2008
  "--min-frac": "minFrac",
2009
- "--seam-tolerance": "seamTolerance"
2009
+ "--seam-tolerance": "seamTolerance",
2010
+ "--erode": "erode"
2010
2011
  };
2011
2012
  var UI_USAGE = `${c.bold("Options for `ui` (all tools read --in <png path or https URL>)")}
2012
2013
  ui extract --out-dir <dir> --names a,b,c (reading order) \u2014 split an
2013
2014
  asset sheet into per-element transparent PNGs + .bbox.json
2014
- sidecars. [--expect N: hard-error unless exactly N
2015
- components] [--min-pixels 2000] [--padding 8] [--chroma 240]
2016
- [--dilate 0]
2015
+ sidecars. Where extract keys the white itself it also
2016
+ defringes the 2px sprite rim (matte un-blend \u2014 kills the
2017
+ white halo over dark scenes). [--expect N: hard-error
2018
+ unless exactly N components] [--min-pixels 2000]
2019
+ [--padding 8] [--chroma 240] [--dilate 0] [--no-defringe]
2017
2020
  ui masks --out-dir <dir> --pairs "name:cX,cY,w,h:aX,aY,w,h[:n];\u2026"
2018
2021
  OR --auto (detect clean/annotated cell pairs itself) \u2014
2019
2022
  clean frame + green-annotated duplicate \u2192 frame + fill mask
2020
2023
  + fillBox + per-segment fillBoxes (+ overlay + provenance
2021
- JSON). [--names a,b,c with --auto] [--registration-
2024
+ JSON). Hard-refuses provable sheet defects (white-painted
2025
+ fill trough, a crop catching a neighboring element).
2026
+ [--names a,b,c with --auto] [--expect N with --auto: every
2027
+ pair must yield exactly N fill components] [--registration-
2022
2028
  tolerance 0.04] [--coverage-min 0.01] [--coverage-max 0.85]
2023
- [--component-min-pixels 100] [--edge-flush-max 0.04]
2029
+ [--component-min-pixels 100]
2024
2030
  ui text-color --box x,y,w,h \u2014 eyedropper a text region: plate color +
2025
2031
  ink candidates as JSON. [--quantize 12] [--ink-dist 60]
2026
2032
  [--min-frac 0.004] [--out <json>] [--crop <png>]
@@ -2029,11 +2035,21 @@ var UI_USAGE = `${c.bold("Options for `ui` (all tools read --in <png path or htt
2029
2035
  ui seams measure a tiling texture's wrap seam (opposite edges vs
2030
2036
  interior detail) \u2192 ratio + verdict as JSON; nonzero exit on
2031
2037
  a visible seam. [--seam-tolerance 3]
2032
- ui audit mechanical HUD-wiring scan (no --in): extracted-but-never-
2033
- wired sprites, generated-but-unreferenced masks, naive
2034
- JS %-width fills, phones-preload-video heuristic. Warn-only
2035
- (findings may be fine \u2014 check them). [--dir public/assets/
2036
- hud] [--src src] [--strict: exit 1 on findings]`;
2038
+ ui plate [--out <png>] \u2014 trace a frame sprite's REAL interior
2039
+ (art footprint + enclosed cavity) into a silhouette mask
2040
+ PNG. Wire it as the CSS plate's mask-image so rgba/
2041
+ backdrop-filter plates fit arbitrary art exactly \u2014 never
2042
+ a bare rounded rectangle. [--erode 2]
2043
+ ui audit mechanical HUD-wiring scan (no --in): mask\u2194frame pairing
2044
+ mismatches (recorded crops disagree \u2014 PROVABLE, exit 1),
2045
+ white-painted meter troughs (baked sheet-white inside a
2046
+ fill channel \u2014 PROVABLE, exit 1), loosened-check stamps,
2047
+ extracted-but-never-wired sprites, generated-but-
2048
+ unreferenced masks, naive JS %-width fills, phones-
2049
+ preload-video heuristic. Everything but the two provable
2050
+ kinds is warn-only (findings may be fine \u2014 check them).
2051
+ [--dir public/assets/hud] [--src src] [--strict: exit 1 on
2052
+ any findings]`;
2037
2053
  async function runUi(opts) {
2038
2054
  const log = createLogger({ quiet: opts.quiet });
2039
2055
  try {
@@ -2053,12 +2069,15 @@ async function runUi(opts) {
2053
2069
  case "seams":
2054
2070
  await uiSeams(opts, log);
2055
2071
  return;
2072
+ case "plate":
2073
+ await uiPlate(opts, log);
2074
+ return;
2056
2075
  case "audit":
2057
2076
  await uiAudit(opts, log);
2058
2077
  return;
2059
2078
  default:
2060
2079
  log.error(
2061
- opts.sub ? `Unknown ui tool "${opts.sub}". Tools: extract, masks, text-color, trim, seams, audit.` : "Missing ui tool. Tools: extract, masks, text-color, trim, seams, audit."
2080
+ opts.sub ? `Unknown ui tool "${opts.sub}". Tools: extract, masks, text-color, trim, seams, plate, audit.` : "Missing ui tool. Tools: extract, masks, text-color, trim, seams, plate, audit."
2062
2081
  );
2063
2082
  log.plain(UI_USAGE);
2064
2083
  process.exitCode = 1;
@@ -2240,6 +2259,33 @@ async function uiExtract(opts, log) {
2240
2259
  band.items.sort((a, b) => a.cx - b.cx);
2241
2260
  sorted.push(...band.items);
2242
2261
  }
2262
+ const nameOf = (i) => i < names.length ? names[i] : `extra-${i - names.length}`;
2263
+ for (let i = 0; i < sorted.length; i++) {
2264
+ const comp = sorted[i];
2265
+ const x0 = Math.max(0, comp.minX - padding);
2266
+ const y0 = Math.max(0, comp.minY - padding);
2267
+ const x1 = Math.min(W, comp.maxX + padding + 1);
2268
+ const y1 = Math.min(H, comp.maxY + padding + 1);
2269
+ for (let j = 0; j < sorted.length; j++) {
2270
+ if (j === i) continue;
2271
+ const other = sorted[j];
2272
+ const oMinX = other.minX + 2;
2273
+ const oMinY = other.minY + 2;
2274
+ const oMaxX = other.maxX - 2;
2275
+ const oMaxY = other.maxY - 2;
2276
+ if (oMinX < x1 && oMaxX >= x0 && oMinY < y1 && oMaxY >= y0) {
2277
+ const w = other.maxX - other.minX + 1;
2278
+ const h = other.maxY - other.minY + 1;
2279
+ fail(
2280
+ `"${nameOf(i)}": the crop (component + ${padding}px padding) catches "${nameOf(j)}" at ${other.minX},${other.minY} (${w}x${h}) \u2014 its edge would ship baked into this sprite. Nothing written. Regenerate the sheet with more margin between elements, or lower --padding.`,
2281
+ {
2282
+ crop: { x: x0, y: y0, w: x1 - x0, h: y1 - y0 },
2283
+ neighbor: { name: nameOf(j), x: other.minX, y: other.minY, w, h }
2284
+ }
2285
+ );
2286
+ }
2287
+ }
2288
+ }
2243
2289
  const extractions = [];
2244
2290
  for (let i = 0; i < sorted.length; i++) {
2245
2291
  const comp = sorted[i];
@@ -2276,6 +2322,10 @@ async function uiExtract(opts, log) {
2276
2322
  }
2277
2323
  }
2278
2324
  }
2325
+ let defringed = false;
2326
+ if (inputMode === "chroma" && opts.defringe !== false) {
2327
+ defringed = defringeAgainstMatte(out) > 0;
2328
+ }
2279
2329
  const outPath = path10.join(outDir, `${name}.png`);
2280
2330
  await writePng(outPath, out);
2281
2331
  const sidecar = {
@@ -2291,7 +2341,8 @@ async function uiExtract(opts, log) {
2291
2341
  },
2292
2342
  cropDims: { w: cropW, h: cropH },
2293
2343
  aspectRatio: +(cropW / cropH).toFixed(3),
2294
- componentPixels: comp.pixels
2344
+ componentPixels: comp.pixels,
2345
+ defringed
2295
2346
  };
2296
2347
  const { name: _n, out: _o, ...sidecarBody } = sidecar;
2297
2348
  await fs10.writeFile(
@@ -2395,7 +2446,7 @@ function assertNotFlush(png, label, maxFrac) {
2395
2446
  const flush2 = Object.entries(edges).filter(([, frac]) => frac > maxFrac);
2396
2447
  if (flush2.length > 0) {
2397
2448
  fail(
2398
- `${label}: crop box is slicing the element \u2014 opaque content is flush against the image edge. Widen the crop box (or pass --edge-flush-max 1 for a deliberate full-bleed asset).`,
2449
+ `${label}: the crop box is slicing the element \u2014 opaque content is flush against the crop edge. The sheet needs more margin here: widen the crop box if there is room, otherwise region-edit or regenerate the sheet with more spacing around this element (see genex-ai-hud troubleshooting), then re-run.`,
2399
2450
  {
2400
2451
  flushEdges: flush2.map(([edge, frac]) => ({ edge, opaqueFraction: Number(frac.toFixed(3)) })),
2401
2452
  maxFraction: maxFrac
@@ -2403,6 +2454,70 @@ function assertNotFlush(png, label, maxFrac) {
2403
2454
  );
2404
2455
  }
2405
2456
  }
2457
+ function whiteTroughFraction(clean2, mask) {
2458
+ let fill = 0;
2459
+ let white = 0;
2460
+ for (let i = 0; i < mask.width * mask.height; i += 1) {
2461
+ const idx = i << 2;
2462
+ if (mask.data[idx + 3] === 0) continue;
2463
+ fill += 1;
2464
+ if (clean2.data[idx + 3] > 200 && clean2.data[idx] >= 240 && clean2.data[idx + 1] >= 240 && clean2.data[idx + 2] >= 240) {
2465
+ white += 1;
2466
+ }
2467
+ }
2468
+ return fill === 0 ? 0 : white / fill;
2469
+ }
2470
+ var WHITE_TROUGH_MAX = 0.4;
2471
+ function detectSheetComponents(image, minPixels) {
2472
+ const { width: W, height: H, data } = image;
2473
+ let hasTransparency = false;
2474
+ for (let i = 0; i < W * H; i += 1) {
2475
+ if (data[i * 4 + 3] < 250) {
2476
+ hasTransparency = true;
2477
+ break;
2478
+ }
2479
+ }
2480
+ const isSubjectIdx = (idx) => {
2481
+ const o = idx * 4;
2482
+ if (data[o + 3] <= ALPHA_TRANSPARENT_MAX) return false;
2483
+ if (hasTransparency) return true;
2484
+ return !(data[o] >= 240 && data[o + 1] >= 240 && data[o + 2] >= 240);
2485
+ };
2486
+ const components = [];
2487
+ scanComponents(W, H, isSubjectIdx, (stats) => {
2488
+ if (stats.pixels < minPixels) return;
2489
+ components.push({
2490
+ minX: stats.minX,
2491
+ minY: stats.minY,
2492
+ maxX: stats.maxX,
2493
+ maxY: stats.maxY,
2494
+ pixels: stats.pixels
2495
+ });
2496
+ });
2497
+ return components;
2498
+ }
2499
+ var OVERLAP_TOLERANCE_PX = 2;
2500
+ function assertNoNeighborInCrop(box, components, label) {
2501
+ for (const comp of components) {
2502
+ const cx = (comp.minX + comp.maxX) / 2;
2503
+ const cy = (comp.minY + comp.maxY) / 2;
2504
+ const centerInside = cx >= box.x && cx < box.x + box.w && cy >= box.y && cy < box.y + box.h;
2505
+ if (centerInside) continue;
2506
+ const nMinX = comp.minX + OVERLAP_TOLERANCE_PX;
2507
+ const nMinY = comp.minY + OVERLAP_TOLERANCE_PX;
2508
+ const nMaxX = comp.maxX - OVERLAP_TOLERANCE_PX;
2509
+ const nMaxY = comp.maxY - OVERLAP_TOLERANCE_PX;
2510
+ const intersects = nMinX < box.x + box.w && nMaxX >= box.x && nMinY < box.y + box.h && nMaxY >= box.y;
2511
+ if (intersects) {
2512
+ const w = comp.maxX - comp.minX + 1;
2513
+ const h = comp.maxY - comp.minY + 1;
2514
+ fail(
2515
+ `${label}: the crop catches another sheet element at ${comp.minX},${comp.minY} (${w}x${h}) \u2014 its edge would ship baked into this asset. Regenerate the sheet with more margin between elements, or tighten the crop so it stays clear of the neighbor.`,
2516
+ { crop: box, neighbor: { x: comp.minX, y: comp.minY, w, h } }
2517
+ );
2518
+ }
2519
+ }
2520
+ }
2406
2521
  var isWhitePx = (data, idx) => data[idx + 3] > 16 && data[idx] >= 245 && data[idx + 1] >= 245 && data[idx + 2] >= 245;
2407
2522
  var isSubjectPx = (data, idx) => data[idx + 3] > 16 && !isWhitePx(data, idx);
2408
2523
  function greenAlpha(r, g, b, a) {
@@ -2677,7 +2792,10 @@ function autoDetectPairs(image, opts, log) {
2677
2792
  name: names[i] ?? `meter-${i + 1}`,
2678
2793
  cleanBox: place(best.cell.minX, best.cell.minY, cw, ch),
2679
2794
  annotatedBox: place(ann.minX, ann.minY, aw, ah),
2680
- expectedComponents: null
2795
+ // D6: --auto used to leave the component count ungated (null). --expect N
2796
+ // now gates every auto pair (a continuous bar is --expect 1; a segmented
2797
+ // meter its segment count).
2798
+ expectedComponents: opts.expect ?? null
2681
2799
  });
2682
2800
  });
2683
2801
  log.dim(
@@ -2698,9 +2816,11 @@ async function uiMasks(opts, log) {
2698
2816
  const minComponentPixels = opts.componentMinPixels ?? 100;
2699
2817
  const registrationTolerance = opts.registrationTolerance ?? 0.04;
2700
2818
  const edgeFlushMax = opts.edgeFlushMax ?? 0.04;
2819
+ const loosened = registrationTolerance > 0.04 || edgeFlushMax > 0.04 || minCoverage < 0.01 || maxCoverage > 0.85;
2701
2820
  await fs10.mkdir(outDir, { recursive: true });
2702
2821
  log.plain(c.bold("genex ui masks"));
2703
2822
  log.dim(` ${input} (${image.width}x${image.height}), ${pairs.length} pair(s) \u2192 ${outDir}`);
2823
+ const sheetComponents = detectSheetComponents(image, opts.minPixels ?? 2e3);
2704
2824
  const results = [];
2705
2825
  for (const pair of pairs) {
2706
2826
  if (pair.cleanBox.w !== pair.annotatedBox.w || pair.cleanBox.h !== pair.annotatedBox.h) {
@@ -2708,6 +2828,8 @@ async function uiMasks(opts, log) {
2708
2828
  }
2709
2829
  assertInside(pair.cleanBox, image, `${pair.name} clean`);
2710
2830
  assertInside(pair.annotatedBox, image, `${pair.name} annotated`);
2831
+ assertNoNeighborInCrop(pair.cleanBox, sheetComponents, `${pair.name} clean frame`);
2832
+ assertNoNeighborInCrop(pair.annotatedBox, sheetComponents, `${pair.name} annotated copy`);
2711
2833
  const clean2 = cropPng(image, pair.cleanBox);
2712
2834
  const annotated = cropPng(image, pair.annotatedBox);
2713
2835
  assertNotFlush(clean2, `${pair.name} clean frame`, edgeFlushMax);
@@ -2732,6 +2854,13 @@ async function uiMasks(opts, log) {
2732
2854
  maxCoverage
2733
2855
  });
2734
2856
  }
2857
+ const whiteTrough = whiteTroughFraction(clean2, converted.png);
2858
+ if (whiteTrough >= WHITE_TROUGH_MAX) {
2859
+ fail(
2860
+ `${pair.name}: the fill channel is painted filled in the frame art \u2014 ${Math.round(whiteTrough * 100)}% of the fill region is opaque near-white in the clean frame. The sheet model ignored "REMOVE the fill": as the value drops, that baked white shows through the mask. Regenerate the sheet (region-edit the bar to an empty trough) and re-run \u2014 do not wire this frame.`,
2861
+ { name: pair.name, whiteTroughFraction: Number(whiteTrough.toFixed(3)) }
2862
+ );
2863
+ }
2735
2864
  const components = greenComponents(converted.png, minComponentPixels);
2736
2865
  if (pair.expectedComponents && components.length !== pair.expectedComponents) {
2737
2866
  fail("Green mask component count mismatch.", {
@@ -2778,6 +2907,9 @@ async function uiMasks(opts, log) {
2778
2907
  bboxDelta: Number(registrationDelta.toFixed(4)),
2779
2908
  tolerance: registrationTolerance
2780
2909
  },
2910
+ // D6: true when any gate tolerance was relaxed beyond its default for
2911
+ // this run — preview/publish read this and warn (they never block).
2912
+ loosened,
2781
2913
  mask: {
2782
2914
  path: maskPath,
2783
2915
  size: { w: converted.png.width, h: converted.png.height },
@@ -2806,11 +2938,16 @@ async function uiMasks(opts, log) {
2806
2938
  }
2807
2939
  }
2808
2940
  const indexPath = path10.join(outDir, "annotated-progress.json");
2809
- await fs10.writeFile(indexPath, `${JSON.stringify({ input, pairs: results }, null, 2)}
2941
+ await fs10.writeFile(indexPath, `${JSON.stringify({ input, loosened, pairs: results }, null, 2)}
2810
2942
  `);
2811
2943
  log.plain("");
2812
2944
  log.success(`Wrote ${pairs.length} frame+mask pair(s) \u2192 ${outDir} (index: ${indexPath}).`);
2813
2945
  log.dim(" Eyeball each *-overlay.png \u2014 green must cover exactly the fill region.");
2946
+ if (loosened) {
2947
+ log.warn(
2948
+ "These masks were produced with loosened checks (a tolerance beyond its default). That is stamped in the metadata; eyeball the wired result in-game \u2014 with the meter NOT at full \u2014 before shipping."
2949
+ );
2950
+ }
2814
2951
  }
2815
2952
  var hexColor = (r, g, b) => `#${[r, g, b].map((v) => Math.round(v).toString(16).padStart(2, "0")).join("")}`;
2816
2953
  var luminance = (r, g, b) => (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
@@ -2988,6 +3125,179 @@ async function uiSeams(opts, log) {
2988
3125
  `Visible ${r.worstAxis} tiling seam in ${name}: the tile boundary jumps ${r.worstRatio}\xD7 the texture's own detail (want \u2264 ${tol}\xD7). Regenerate it seamless/tileable. Don't hide it behind a hand-picked UV repeat \u2014 that trades a seam for a stretched texture (scale belongs in the UVs: worldUV, genex-ai-texture).`
2989
3126
  );
2990
3127
  }
3128
+ function traceInterior(png, erode = 0) {
3129
+ const { width: W, height: H, data } = png;
3130
+ const opaque = (idx) => data[idx * 4 + 3] > ALPHA_TRANSPARENT_MAX;
3131
+ const visited = new Uint8Array(W * H);
3132
+ const queue = new Int32Array(W * H);
3133
+ const inRegion = (idx) => !opaque(idx);
3134
+ for (let x = 0; x < W; x++) {
3135
+ for (const y of [0, H - 1]) {
3136
+ const idx = y * W + x;
3137
+ if (!visited[idx] && inRegion(idx)) floodFill(W, H, visited, queue, x, y, inRegion);
3138
+ }
3139
+ }
3140
+ for (let y = 0; y < H; y++) {
3141
+ for (const x of [0, W - 1]) {
3142
+ const idx = y * W + x;
3143
+ if (!visited[idx] && inRegion(idx)) floodFill(W, H, visited, queue, x, y, inRegion);
3144
+ }
3145
+ }
3146
+ let interior = new Uint8Array(W * H);
3147
+ for (let i = 0; i < W * H; i++) interior[i] = visited[i] ? 0 : 1;
3148
+ for (let pass = 0; pass < erode; pass++) {
3149
+ const next = new Uint8Array(W * H);
3150
+ for (let y = 0; y < H; y++) {
3151
+ for (let x = 0; x < W; x++) {
3152
+ const i = y * W + x;
3153
+ if (!interior[i]) continue;
3154
+ const ok = x > 0 && interior[i - 1] && x < W - 1 && interior[i + 1] && y > 0 && interior[i - W] && y < H - 1 && interior[i + W];
3155
+ if (ok) next[i] = 1;
3156
+ }
3157
+ }
3158
+ interior = next;
3159
+ }
3160
+ return interior;
3161
+ }
3162
+ async function uiPlate(opts, log) {
3163
+ const input = requireOpt(opts.input, "--in", "plate");
3164
+ let outPath = opts.out;
3165
+ if (!outPath) {
3166
+ if (/^https?:\/\//i.test(input)) fail("--in is a URL \u2014 pass --out <png> to say where the plate mask goes.");
3167
+ outPath = input.replace(/\.png$/i, "") + "-plate.png";
3168
+ }
3169
+ const erode = opts.erode ?? 2;
3170
+ const png = await loadPng(input);
3171
+ const { width: W, height: H } = png;
3172
+ const interior = traceInterior(png, erode);
3173
+ let count = 0;
3174
+ const out = new PNG2({ width: W, height: H });
3175
+ for (let i = 0; i < W * H; i++) {
3176
+ const o = i * 4;
3177
+ out.data[o] = 255;
3178
+ out.data[o + 1] = 255;
3179
+ out.data[o + 2] = 255;
3180
+ out.data[o + 3] = interior[i] ? 255 : 0;
3181
+ if (interior[i]) count++;
3182
+ }
3183
+ if (count === 0) {
3184
+ fail("No interior found \u2014 the image is fully transparent (or erode ate everything). Check --in / lower --erode.");
3185
+ }
3186
+ await writePng(outPath, out);
3187
+ const name = path10.basename(outPath);
3188
+ log.plain(c.bold("genex ui plate"));
3189
+ log.success(`${outPath} ${W}x${H}, interior ${(count / (W * H) * 100).toFixed(1)}% (erode ${erode}px)`);
3190
+ log.dim(" Wire it as the plate's silhouette (same box as the frame <img>, plate UNDER the art):");
3191
+ log.dim(` .plate { position:absolute; inset:0; background:rgba(14,10,22,.55); backdrop-filter:blur(5px);`);
3192
+ log.dim(` -webkit-mask-image:url(${name}); mask-image:url(${name});`);
3193
+ log.dim(` -webkit-mask-size:100% 100%; mask-size:100% 100%; mask-repeat:no-repeat; }`);
3194
+ process.stdout.write(
3195
+ `${JSON.stringify({ out: outPath, width: W, height: H, interiorFraction: Number((count / (W * H)).toFixed(4)), erode })}
3196
+ `
3197
+ );
3198
+ }
3199
+ var DEFRINGE_BAND_PX = 2;
3200
+ function defringeAgainstMatte(png, matte = { r: 255, g: 255, b: 255 }) {
3201
+ const { width: W, height: H, data } = png;
3202
+ const opaque = (i) => data[i * 4 + 3] > ALPHA_TRANSPARENT_MAX;
3203
+ const dist = new Uint8Array(W * H);
3204
+ let ring = [];
3205
+ for (let y = 0; y < H; y++) {
3206
+ for (let x = 0; x < W; x++) {
3207
+ const i = y * W + x;
3208
+ if (!opaque(i)) continue;
3209
+ const nearTransparent = x > 0 && !opaque(i - 1) || x < W - 1 && !opaque(i + 1) || y > 0 && !opaque(i - W) || y < H - 1 && !opaque(i + W) || x === 0 || x === W - 1 || y === 0 || y === H - 1;
3210
+ if (nearTransparent) {
3211
+ dist[i] = 1;
3212
+ ring.push(i);
3213
+ }
3214
+ }
3215
+ }
3216
+ for (let d = 2; d <= DEFRINGE_BAND_PX; d++) {
3217
+ const next = [];
3218
+ for (const i of ring) {
3219
+ const x = i % W;
3220
+ const y = i / W | 0;
3221
+ for (const n of [x > 0 ? i - 1 : -1, x < W - 1 ? i + 1 : -1, y > 0 ? i - W : -1, y < H - 1 ? i + W : -1]) {
3222
+ if (n >= 0 && opaque(n) && dist[n] === 0) {
3223
+ dist[n] = d;
3224
+ next.push(n);
3225
+ }
3226
+ }
3227
+ }
3228
+ ring = next;
3229
+ }
3230
+ const CH = [0, 1, 2];
3231
+ const matteCh = [matte.r, matte.g, matte.b];
3232
+ let rewritten = 0;
3233
+ for (let y = 0; y < H; y++) {
3234
+ for (let x = 0; x < W; x++) {
3235
+ const i = y * W + x;
3236
+ if (dist[i] === 0) continue;
3237
+ const cIn = [0, 0, 0];
3238
+ let n = 0;
3239
+ outer: for (let r = 1; r <= 6; r++) {
3240
+ for (let dy = -r; dy <= r; dy++) {
3241
+ for (let dx = -r; dx <= r; dx++) {
3242
+ if (Math.max(Math.abs(dx), Math.abs(dy)) !== r) continue;
3243
+ const nx = x + dx;
3244
+ const ny = y + dy;
3245
+ if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
3246
+ const ni = ny * W + nx;
3247
+ if (!opaque(ni) || dist[ni] !== 0) continue;
3248
+ cIn[0] += data[ni * 4];
3249
+ cIn[1] += data[ni * 4 + 1];
3250
+ cIn[2] += data[ni * 4 + 2];
3251
+ n++;
3252
+ if (n >= 24) break outer;
3253
+ }
3254
+ }
3255
+ }
3256
+ if (n === 0) continue;
3257
+ for (const ch of CH) cIn[ch] = cIn[ch] / n;
3258
+ let alphaSum = 0;
3259
+ let alphaN = 0;
3260
+ for (const ch of CH) {
3261
+ const contrast = matteCh[ch] - cIn[ch];
3262
+ if (Math.abs(contrast) < 30) continue;
3263
+ const a = (matteCh[ch] - data[i * 4 + ch]) / contrast;
3264
+ alphaSum += Math.min(1, Math.max(0, a));
3265
+ alphaN++;
3266
+ }
3267
+ if (alphaN === 0) continue;
3268
+ const alpha = alphaSum / alphaN;
3269
+ if (alpha > 0.98) continue;
3270
+ data[i * 4] = Math.round(cIn[0]);
3271
+ data[i * 4 + 1] = Math.round(cIn[1]);
3272
+ data[i * 4 + 2] = Math.round(cIn[2]);
3273
+ data[i * 4 + 3] = Math.round(alpha * 255);
3274
+ rewritten++;
3275
+ }
3276
+ }
3277
+ return rewritten;
3278
+ }
3279
+ var HARD_AUDIT_KINDS = /* @__PURE__ */ new Set([
3280
+ "mask-frame-mismatch",
3281
+ "white-trough"
3282
+ ]);
3283
+ var TROUGH_OPAQUE_MIN_ALPHA = 200;
3284
+ var TROUGH_WHITE_MIN = 235;
3285
+ var TROUGH_WHITE_MAX_FRACTION = 0.25;
3286
+ function whiteTroughAuditFraction(frame, mask) {
3287
+ const interior = traceInterior(frame, 0);
3288
+ let channel = 0;
3289
+ let white = 0;
3290
+ for (let i = 0; i < mask.width * mask.height; i++) {
3291
+ if (mask.data[i * 4 + 3] === 0) continue;
3292
+ if (!interior[i]) continue;
3293
+ channel++;
3294
+ const o = i * 4;
3295
+ if (frame.data[o + 3] > TROUGH_OPAQUE_MIN_ALPHA && frame.data[o] >= TROUGH_WHITE_MIN && frame.data[o + 1] >= TROUGH_WHITE_MIN && frame.data[o + 2] >= TROUGH_WHITE_MIN) {
3296
+ white++;
3297
+ }
3298
+ }
3299
+ return channel === 0 ? 0 : white / channel;
3300
+ }
2991
3301
  var AUDIT_SRC_EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".css", ".html"]);
2992
3302
  async function walkFiles(dir) {
2993
3303
  const out = [];
@@ -3050,6 +3360,92 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
3050
3360
  const referenced = (basename) => sources.some((s) => s.text.includes(basename));
3051
3361
  const findings = [...viewportFindings];
3052
3362
  const assetFiles = await walkFiles(absAssets);
3363
+ const metaByName = /* @__PURE__ */ new Map();
3364
+ const bboxByPng = /* @__PURE__ */ new Map();
3365
+ for (const p of assetFiles) {
3366
+ const base = path10.basename(p);
3367
+ const metaMatch = /^(.+)\.annotated-progress\.json$/.exec(base);
3368
+ if (metaMatch) {
3369
+ try {
3370
+ const meta = JSON.parse(await fs10.readFile(p, "utf8"));
3371
+ metaByName.set(metaMatch[1], {
3372
+ cleanCrop: meta.clean?.crop ?? null,
3373
+ loosened: meta.loosened === true
3374
+ });
3375
+ } catch {
3376
+ }
3377
+ continue;
3378
+ }
3379
+ if (base.endsWith(".bbox.json")) {
3380
+ try {
3381
+ const sidecar = JSON.parse(await fs10.readFile(p, "utf8"));
3382
+ if (sidecar.sheetBBox) bboxByPng.set(base.replace(/\.bbox\.json$/, ".png"), sidecar.sheetBBox);
3383
+ } catch {
3384
+ }
3385
+ }
3386
+ }
3387
+ for (const [name, meta] of metaByName) {
3388
+ if (!meta.cleanCrop || !referenced(`${name}-mask.png`)) continue;
3389
+ for (const p of assetFiles) {
3390
+ const base = path10.basename(p);
3391
+ if (!base.toLowerCase().endsWith(".png")) continue;
3392
+ if (base !== `${name}.png` && !base.startsWith(`${name}-`)) continue;
3393
+ if (/-mask\.png$|-frame\.png$|-overlay\.png$|-annotated-source\.png$/.test(base)) continue;
3394
+ if (!referenced(base)) continue;
3395
+ const cut = bboxByPng.get(base);
3396
+ if (!cut) continue;
3397
+ const crop = meta.cleanCrop;
3398
+ const delta = Math.max(
3399
+ Math.abs(cut.x - crop.x),
3400
+ Math.abs(cut.y - crop.y),
3401
+ Math.abs(cut.w - crop.w),
3402
+ Math.abs(cut.h - crop.h)
3403
+ );
3404
+ if (delta > 4) {
3405
+ findings.push({
3406
+ kind: "mask-frame-mismatch",
3407
+ message: `${name}: ${name}-mask.png was derived against the sheet crop ${crop.x},${crop.y} (${crop.w}x${crop.h}), but the shipped ${base} was cut at ${cut.x},${cut.y} (${cut.w}x${cut.h}) \u2014 stretched over one box, the fill floats off the frame. Re-derive the mask against the shipped frame (genex ui masks --in <sheet> \u2026) or ship ${name}-frame.png (the crop this mask matches).`
3408
+ });
3409
+ }
3410
+ }
3411
+ }
3412
+ const pngByBase = /* @__PURE__ */ new Map();
3413
+ for (const p of assetFiles) {
3414
+ const base = path10.basename(p);
3415
+ if (base.toLowerCase().endsWith(".png")) pngByBase.set(base, p);
3416
+ }
3417
+ for (const [maskBase, maskPath] of pngByBase) {
3418
+ const m = /^(.+)-mask\.png$/.exec(maskBase);
3419
+ if (!m) continue;
3420
+ const name = m[1];
3421
+ for (const frameBase of [`${name}-frame.png`, `${name}.png`]) {
3422
+ const framePath = pngByBase.get(frameBase);
3423
+ if (!framePath) continue;
3424
+ let frame;
3425
+ let mask;
3426
+ try {
3427
+ frame = PNG2.sync.read(await fs10.readFile(framePath));
3428
+ mask = PNG2.sync.read(await fs10.readFile(maskPath));
3429
+ } catch {
3430
+ continue;
3431
+ }
3432
+ if (frame.width !== mask.width || frame.height !== mask.height) continue;
3433
+ const fraction = whiteTroughAuditFraction(frame, mask);
3434
+ if (fraction > TROUGH_WHITE_MAX_FRACTION) {
3435
+ findings.push({
3436
+ kind: "white-trough",
3437
+ message: `${frameBase}: the fill channel contains baked sheet-white (${Math.round(fraction * 100)}% of the channel \u2014 the model drew the trough white and the cleaner can't reach enclosed regions). Repair: regenerate the sheet with the empty-dark-track rule, or region-repaint the trough (genex image "<in-style empty track>" --edit <sheet-url> [--inpaint <channel-mask>]), then re-clean and re-extract. Do not ship this frame.`
3438
+ });
3439
+ }
3440
+ }
3441
+ }
3442
+ const loosenedNames = [...metaByName.entries()].filter(([, meta]) => meta.loosened).map(([name]) => name);
3443
+ if (loosenedNames.length > 0) {
3444
+ findings.push({
3445
+ kind: "loosened-masks",
3446
+ message: `HUD masks for ${loosenedNames.join(", ")} were produced with loosened checks \u2014 eyeball each wired meter in-game, NOT at full value, before shipping.`
3447
+ });
3448
+ }
3053
3449
  const maskReported = /* @__PURE__ */ new Set();
3054
3450
  for (const p of assetFiles) {
3055
3451
  const m = /^(.+)\.annotated-progress\.json$/.exec(path10.basename(p));
@@ -3113,11 +3509,14 @@ async function uiAudit(opts, log) {
3113
3509
  log.success("HUD wiring audit clean \u2014 every asset referenced, no naive %-fill patterns.");
3114
3510
  return;
3115
3511
  }
3116
- log.warn(
3117
- `${findings.length} finding(s). These are heuristics \u2014 check each one; some may be fine:`
3118
- );
3119
- for (const finding of findings) log.warn(` [${finding.kind}] ${finding.message}`);
3120
- if (opts.strict) process.exitCode = 1;
3512
+ const hard = findings.filter((finding) => HARD_AUDIT_KINDS.has(finding.kind));
3513
+ const soft = findings.filter((finding) => !HARD_AUDIT_KINDS.has(finding.kind));
3514
+ for (const finding of hard) log.error(`[${finding.kind}] ${finding.message}`);
3515
+ if (soft.length > 0) {
3516
+ log.warn(`${soft.length} finding(s). These are heuristics \u2014 check each one; some may be fine:`);
3517
+ for (const finding of soft) log.warn(` [${finding.kind}] ${finding.message}`);
3518
+ }
3519
+ if (opts.strict || hard.length > 0) process.exitCode = 1;
3121
3520
  }
3122
3521
 
3123
3522
  // src/lib/deploy.ts
@@ -3961,7 +4360,7 @@ async function detectSurfaceScan(cwd = process.cwd()) {
3961
4360
  }
3962
4361
  return found;
3963
4362
  }
3964
- var WIRED_BY_URL = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "texture", "video"]);
4363
+ var WIRED_BY_URL = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "music", "texture", "video"]);
3965
4364
  var UNPICKED_AFTER_MS = 15 * 60 * 1e3;
3966
4365
  async function detectGenerationAudit(cwd = process.cwd()) {
3967
4366
  const ledger = await readLedger(cwd);
@@ -4285,7 +4684,86 @@ async function runPreview(opts) {
4285
4684
  }
4286
4685
 
4287
4686
  // src/commands/generate.ts
4687
+ import fs14 from "fs/promises";
4688
+ import path14 from "path";
4689
+ import { PNG as PNG4 } from "pngjs";
4690
+
4691
+ // src/lib/glass.ts
4288
4692
  import { PNG as PNG3 } from "pngjs";
4693
+ var GLASS_PROMPT_SUFFIX = " Rendered on a perfectly flat, uniform, solid pure magenta background, hex #FF00FF, like a studio chroma-key screen: absolutely no shading, no gradient, no vignette, no glow, no shadows on the background. Subject centered, sharp edge silhouette.";
4694
+ var GLASS_MIN_FLATNESS = 0.99;
4695
+ var GLASS_MIN_KEY_STRENGTH = 180;
4696
+ var FLATNESS_DEV_MAX = 12;
4697
+ var clamp255 = (v) => v < 0 ? 0 : v > 255 ? 255 : Math.round(v);
4698
+ function median(values) {
4699
+ const s = values.slice().sort((a, b) => a - b);
4700
+ return s[Math.floor(s.length / 2)] ?? 0;
4701
+ }
4702
+ function solveMagentaGlass(source) {
4703
+ const { width: W, height: H, data } = source;
4704
+ const ringW = Math.max(2, Math.round(Math.min(W, H) * 0.04));
4705
+ const rs = [];
4706
+ const gs = [];
4707
+ const bs = [];
4708
+ const ringIdx = [];
4709
+ for (let y = 0; y < H; y++) {
4710
+ for (let x = 0; x < W; x++) {
4711
+ const inRing = x < ringW || y < ringW || x >= W - ringW || y >= H - ringW;
4712
+ if (!inRing) continue;
4713
+ const i = (y * W + x) * 4;
4714
+ ringIdx.push(i);
4715
+ rs.push(data[i]);
4716
+ gs.push(data[i + 1]);
4717
+ bs.push(data[i + 2]);
4718
+ }
4719
+ }
4720
+ const K = { r: median(rs), g: median(gs), b: median(bs) };
4721
+ const keyHex = `#${[K.r, K.g, K.b].map((v) => v.toString(16).padStart(2, "0")).join("")}`;
4722
+ const keyStrength = Math.min(K.r, K.b) - K.g;
4723
+ let flat = 0;
4724
+ for (const i of ringIdx) {
4725
+ const dev = Math.max(
4726
+ Math.abs(data[i] - K.r),
4727
+ Math.abs(data[i + 1] - K.g),
4728
+ Math.abs(data[i + 2] - K.b)
4729
+ );
4730
+ if (dev < FLATNESS_DEV_MAX) flat++;
4731
+ }
4732
+ const flatness = ringIdx.length === 0 ? 0 : flat / ringIdx.length;
4733
+ const base = { keyColor: K, keyHex, keyStrength, flatness };
4734
+ if (flatness < GLASS_MIN_FLATNESS || keyStrength < GLASS_MIN_KEY_STRENGTH) {
4735
+ return {
4736
+ ...base,
4737
+ png: source,
4738
+ ok: false,
4739
+ rejected: `backdrop not keyable (flatness ${flatness.toFixed(3)}, key strength ${keyStrength} \u2014 want \u2265${GLASS_MIN_FLATNESS}/${GLASS_MIN_KEY_STRENGTH}): the model shaded the magenta screen instead of painting it flat. Regenerate (same prompt \u2014 this is a rare roll), or steer harder: keep the --glass suffix intact and avoid magenta/pink art in the subject.`
4740
+ };
4741
+ }
4742
+ const out = new PNG3({ width: W, height: H });
4743
+ const mKey = Math.max(1, keyStrength);
4744
+ for (let p = 0; p < W * H; p++) {
4745
+ const i = p * 4;
4746
+ const R = data[i];
4747
+ const G = data[i + 1];
4748
+ const B = data[i + 2];
4749
+ const m = Math.max(0, Math.min(R, B) - G);
4750
+ let a = 1 - m / mKey;
4751
+ if (a < 0) a = 0;
4752
+ if (a > 1) a = 1;
4753
+ if (a <= 0.02) {
4754
+ out.data[i] = 0;
4755
+ out.data[i + 1] = 0;
4756
+ out.data[i + 2] = 0;
4757
+ out.data[i + 3] = 0;
4758
+ continue;
4759
+ }
4760
+ out.data[i] = clamp255((R - (1 - a) * K.r) / a);
4761
+ out.data[i + 1] = clamp255((G - (1 - a) * K.g) / a);
4762
+ out.data[i + 2] = clamp255((B - (1 - a) * K.b) / a);
4763
+ out.data[i + 3] = Math.round(a * 255);
4764
+ }
4765
+ return { ...base, png: out, ok: true };
4766
+ }
4289
4767
 
4290
4768
  // src/lib/open.ts
4291
4769
  import { spawn as spawn4 } from "child_process";
@@ -4366,10 +4844,22 @@ async function* readSSE(body) {
4366
4844
 
4367
4845
  // src/commands/generate.ts
4368
4846
  var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
4847
+ var SKYBOX_ENVIRONMENT_SUFFIX = ". Pure environment: sky, clouds, atmosphere, distant horizon haze. No buildings, no structures, no trees, no objects, no ground detail.";
4848
+ var SKYBOX_PROMPT_MAX = 2e3;
4849
+ function applySkyboxGuard(prompt, raw) {
4850
+ if (raw) return { prompt, guarded: false };
4851
+ const guarded = `${prompt}${SKYBOX_ENVIRONMENT_SUFFIX}`;
4852
+ if (guarded.length > SKYBOX_PROMPT_MAX) return { prompt, guarded: false };
4853
+ return { prompt: guarded, guarded: true };
4854
+ }
4369
4855
  function buildGenOptions(kind, opts) {
4370
4856
  const options = { ...opts.generationOptions };
4371
4857
  if (kind === "texture" && opts.terrain) options.terrain = true;
4372
- if (kind === "sfx" && opts.duration) options.durationSeconds = opts.duration;
4858
+ if ((kind === "sfx" || kind === "music") && opts.duration) options.durationSeconds = opts.duration;
4859
+ if (kind === "voice") {
4860
+ if (opts.voice) options.voice = opts.voice;
4861
+ if (opts.voiceId) options.voiceId = opts.voiceId;
4862
+ }
4373
4863
  if (kind === "image") {
4374
4864
  if (opts.transparent) options.transparent = true;
4375
4865
  if (opts.aspect) options.aspect = opts.aspect;
@@ -4388,6 +4878,11 @@ function buildGenOptions(kind, opts) {
4388
4878
  options.op = "remove-bg";
4389
4879
  options.bgMode = opts.bgMode ?? "sheet";
4390
4880
  }
4881
+ if (opts.upscaleUrl) {
4882
+ options.sourceUrl = opts.upscaleUrl;
4883
+ options.op = "upscale";
4884
+ }
4885
+ if (opts.inpaintUrl) options.maskUrl = opts.inpaintUrl;
4391
4886
  if (opts.removeBackground) options.removeBackground = true;
4392
4887
  if (opts.bgMode) options.bgMode = opts.bgMode;
4393
4888
  }
@@ -4403,12 +4898,45 @@ function buildGenOptions(kind, opts) {
4403
4898
  }
4404
4899
  async function runGenerate(kind, opts) {
4405
4900
  const log = createLogger({ quiet: opts.quiet || opts.json });
4406
- const prompt = opts.prompt?.trim();
4407
- if (!prompt) {
4901
+ const typedPrompt = opts.prompt?.trim();
4902
+ if (!typedPrompt) {
4408
4903
  log.error(`Missing prompt. Usage: ${c.cyan(`genex ${kind} "<prompt>"`)}`);
4409
4904
  process.exitCode = 1;
4410
4905
  return;
4411
4906
  }
4907
+ const skyboxGuard = kind === "skybox" ? applySkyboxGuard(typedPrompt, opts.raw) : void 0;
4908
+ let prompt = skyboxGuard?.prompt ?? typedPrompt;
4909
+ if (kind === "image" && opts.glass) {
4910
+ const conflict = opts.transparent ? "--transparent" : opts.removeBackground ? "--remove-bg" : opts.editUrl ? "--edit" : opts.cleanUrl ? "--clean" : opts.upscaleUrl ? "--upscale" : opts.noWait ? "--no-wait" : void 0;
4911
+ if (conflict) {
4912
+ log.error(
4913
+ conflict === "--no-wait" ? "--glass runs a local key-solve on the finished image \u2014 it has to wait. Drop --no-wait." : `--glass generates a NEW panel on a magenta key screen \u2014 it cannot combine with ${conflict}. The solved RGBA ships real translucency; no removal pass applies.`
4914
+ );
4915
+ process.exitCode = 1;
4916
+ return;
4917
+ }
4918
+ prompt = `${prompt}${GLASS_PROMPT_SUFFIX}`;
4919
+ }
4920
+ if (kind === "image" && opts.inpaintUrl && !/^https?:\/\//i.test(opts.inpaintUrl)) {
4921
+ if (!opts.editUrl) {
4922
+ log.error("--inpaint targets an edit \u2014 pass --edit <asset-url> alongside it.");
4923
+ process.exitCode = 1;
4924
+ return;
4925
+ }
4926
+ try {
4927
+ const bytes = await fs14.readFile(opts.inpaintUrl);
4928
+ opts = { ...opts, inpaintUrl: `data:image/png;base64,${bytes.toString("base64")}` };
4929
+ } catch {
4930
+ log.error(`Couldn't read the --inpaint mask at ${opts.inpaintUrl}.`);
4931
+ process.exitCode = 1;
4932
+ return;
4933
+ }
4934
+ }
4935
+ if (kind === "voice" && prompt.length > 1e3) {
4936
+ log.warn(
4937
+ `Voice text is ${prompt.length} chars \u2014 the server clamps at 1000 (billing follows the clamp). Split longer copy into separate lines.`
4938
+ );
4939
+ }
4412
4940
  const token = opts.token ?? await readUserToken(opts.envPath);
4413
4941
  if (!token) {
4414
4942
  log.error("Not authorized. Run `genex init` first to sign in.");
@@ -4420,6 +4948,7 @@ async function runGenerate(kind, opts) {
4420
4948
  const options = buildGenOptions(kind, opts);
4421
4949
  log.plain(c.bold(`genex ${kind}`));
4422
4950
  log.dim(` ${prompt}`);
4951
+ if (skyboxGuard?.guarded) log.dim(" \u21B3 environment-only guard applied (--raw to disable)");
4423
4952
  log.plain("");
4424
4953
  let id;
4425
4954
  try {
@@ -4472,9 +5001,104 @@ async function runGenerate(kind, opts) {
4472
5001
  }
4473
5002
  return id;
4474
5003
  }
5004
+ if (kind === "image" && opts.glass) {
5005
+ if (!opts.json) log.step("Generating\u2026 (this can take up to a minute)");
5006
+ const view = await waitForTerminalView(
5007
+ apiUrl,
5008
+ token,
5009
+ id,
5010
+ kind,
5011
+ opts.json ? () => {
5012
+ } : (p) => log.dim(` ${p}%`)
5013
+ );
5014
+ if (!view) {
5015
+ if (opts.json) writeJson({ kind, id, status: "failed", error: "Timed out waiting for the generation." });
5016
+ else log.error("Timed out waiting for the generation.");
5017
+ process.exitCode = 1;
5018
+ return id;
5019
+ }
5020
+ await reportGlassTerminal(view, opts.outDir ?? ".", log, opts.json ?? false);
5021
+ return id;
5022
+ }
4475
5023
  await awaitAndReport(apiUrl, token, id, kind, log, opts.open, opts.json);
4476
5024
  return id;
4477
5025
  }
5026
+ async function reportGlassTerminal(view, outDir, log, json) {
5027
+ const files = (view.files ?? []).filter((f) => /^image-(main|alt-\d+)$/.test(f.role));
5028
+ if (view.status !== "completed" || files.length === 0) {
5029
+ await reportTerminal("image", view, log, false, json);
5030
+ return;
5031
+ }
5032
+ await recordTerminal(view.id, "completed", files.map((f) => f.url));
5033
+ await fs14.mkdir(outDir, { recursive: true });
5034
+ const solved = [];
5035
+ for (let i = 0; i < files.length; i++) {
5036
+ const f = files[i];
5037
+ let png;
5038
+ try {
5039
+ const res = await fetch(f.url);
5040
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
5041
+ png = PNG4.sync.read(Buffer.from(await res.arrayBuffer()));
5042
+ } catch (err) {
5043
+ solved.push({
5044
+ path: null,
5045
+ url: f.url,
5046
+ keyHex: "",
5047
+ keyStrength: 0,
5048
+ flatness: 0,
5049
+ rejected: `couldn't fetch the candidate (${String(err)}) \u2014 re-run later; the URL stays live.`
5050
+ });
5051
+ continue;
5052
+ }
5053
+ const r = solveMagentaGlass(png);
5054
+ if (!r.ok) {
5055
+ solved.push({
5056
+ path: null,
5057
+ url: f.url,
5058
+ keyHex: r.keyHex,
5059
+ keyStrength: r.keyStrength,
5060
+ flatness: r.flatness,
5061
+ rejected: r.rejected
5062
+ });
5063
+ continue;
5064
+ }
5065
+ const outPath = path14.join(outDir, `glass-${i + 1}.png`);
5066
+ await fs14.writeFile(outPath, PNG4.sync.write(r.png));
5067
+ solved.push({
5068
+ path: outPath,
5069
+ url: f.url,
5070
+ keyHex: r.keyHex,
5071
+ keyStrength: r.keyStrength,
5072
+ flatness: r.flatness
5073
+ });
5074
+ }
5075
+ const shipped = solved.filter((s) => s.path !== null);
5076
+ if (json) {
5077
+ writeJson({ kind: "image", id: view.id, status: view.status, glass: solved });
5078
+ if (shipped.length === 0) process.exitCode = 1;
5079
+ return;
5080
+ }
5081
+ log.plain("");
5082
+ if (shipped.length > 0) {
5083
+ log.success(`Solved ${shipped.length} glass panel(s) \u2014 real translucency baked in the alpha:`);
5084
+ }
5085
+ for (const s of solved) {
5086
+ if (s.path) {
5087
+ log.plain(` ${s.path} ${c.dim(`key ${s.keyHex}, strength ${s.keyStrength}, flatness ${s.flatness.toFixed(3)}`)}`);
5088
+ } else {
5089
+ log.error(`Candidate rejected: ${s.rejected}`);
5090
+ log.dim(` raw candidate stays at ${s.url}`);
5091
+ }
5092
+ }
5093
+ if (shipped.length > 0) {
5094
+ log.plain("");
5095
+ log.dim(
5096
+ " Wire the PNG like any sprite (TextureLoader / <img>): its alpha IS the glass \u2014 no CSS opacity, no plate underneath. See genex-ai-image (--glass) and genex-ai-menu."
5097
+ );
5098
+ } else {
5099
+ process.exitCode = 1;
5100
+ }
5101
+ }
4478
5102
  async function awaitAndReport(apiUrl, token, id, kind, log, open = false, json = false) {
4479
5103
  if (!json) log.step("Generating\u2026 (this can take up to a minute)");
4480
5104
  const view = await waitForTerminalView(
@@ -4658,7 +5282,7 @@ async function reportTextureSeam(url, log) {
4658
5282
  try {
4659
5283
  const res = await fetch(url);
4660
5284
  if (!res.ok) return;
4661
- const png = PNG3.sync.read(Buffer.from(await res.arrayBuffer()));
5285
+ const png = PNG4.sync.read(Buffer.from(await res.arrayBuffer()));
4662
5286
  if (png.width < 4 || png.height < 4) return;
4663
5287
  const r = measureSeam(png);
4664
5288
  if (r.seamless) {
@@ -4682,6 +5306,8 @@ function printHint(kind, view, files, log) {
4682
5306
  model: `Standard GLB \u2014 load with GLTFLoader straight from the URL \u2014 see the genex-ai-model skill. url = "${url}"`,
4683
5307
  skybox: `Load as an equirectangular texture \u2192 scene.background + scene.environment \u2014 see genex-ai-skybox. url = "${url}"`,
4684
5308
  sfx: `Load with AudioLoader into a THREE.PositionalAudio (camera needs an AudioListener) \u2014 see genex-ai-sfx. url = "${url}"`,
5309
+ music: `Load with AudioLoader into a non-positional THREE.Audio: setLoop(true), setVolume(0.30), start on the first user gesture \u2014 and the settings screen MUST have Music + SFX volume sliders \u2014 see genex-ai-music. url = "${url}"`,
5310
+ voice: `Load with AudioLoader into a THREE.Audio (or PositionalAudio at the speaker); voice lines are content \u2014 keep them short, SUBTITLED, and skippable \u2014 see genex-ai-voice. url = "${url}"`,
4685
5311
  texture: `Load each map with TextureLoader (RepeatWrapping) into a MeshStandardMaterial \u2014 see genex-ai-texture. Use the URLs above by role.`,
4686
5312
  image: `Load with TextureLoader (set colorSpace = SRGBColorSpace) onto any mesh/plane/sprite \u2014 see genex-ai-image. url = "${url}"`,
4687
5313
  video: `Wire an HTMLVideoElement (crossOrigin="anonymous", muted, loop, playsInline) into a THREE.VideoTexture \u2014 see genex-ai-video. url = "${url}"`,
@@ -5016,8 +5642,8 @@ async function toRow(e, v, cwd) {
5016
5642
  }
5017
5643
 
5018
5644
  // src/commands/controller.ts
5019
- import fs15 from "fs/promises";
5020
- import path15 from "path";
5645
+ import fs16 from "fs/promises";
5646
+ import path16 from "path";
5021
5647
 
5022
5648
  // ../../packages/meshy-animation-catalog/src/index.ts
5023
5649
  import { createHash } from "crypto";
@@ -14147,9 +14773,9 @@ function searchMeshyAnimations(query, options = {}) {
14147
14773
  }
14148
14774
 
14149
14775
  // src/lib/anims.ts
14150
- import fs14 from "fs/promises";
14151
- import path14 from "path";
14152
- var ANIMS_DEST = path14.join("public", "assets", "anims");
14776
+ import fs15 from "fs/promises";
14777
+ import path15 from "path";
14778
+ var ANIMS_DEST = path15.join("public", "assets", "anims");
14153
14779
  var HIDDEN_TAG = "reference";
14154
14780
  async function runAnims(opts) {
14155
14781
  const log = createLogger({ quiet: opts.quiet });
@@ -14165,7 +14791,7 @@ async function runAnims(opts) {
14165
14791
  printCatalog(log, manifest, selectors);
14166
14792
  return;
14167
14793
  }
14168
- const controllerMarker = path14.join(root, "src", "controllers", "character");
14794
+ const controllerMarker = path15.join(root, "src", "controllers", "character");
14169
14795
  if (!await exists2(controllerMarker)) {
14170
14796
  log.error(
14171
14797
  `No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
@@ -14174,11 +14800,11 @@ async function runAnims(opts) {
14174
14800
  process.exitCode = 1;
14175
14801
  return;
14176
14802
  }
14177
- const destDir = path14.join(root, ANIMS_DEST);
14178
- const gameManifestPath = path14.join(destDir, "manifest.json");
14803
+ const destDir = path15.join(root, ANIMS_DEST);
14804
+ const gameManifestPath = path15.join(destDir, "manifest.json");
14179
14805
  if (opts.reset) {
14180
- await fs14.rm(destDir, { recursive: true, force: true });
14181
- log.step(`Cleared ${c.cyan(ANIMS_DEST + path14.sep)} (--reset)`);
14806
+ await fs15.rm(destDir, { recursive: true, force: true });
14807
+ log.step(`Cleared ${c.cyan(ANIMS_DEST + path15.sep)} (--reset)`);
14182
14808
  }
14183
14809
  if (selectors.length === 0) {
14184
14810
  const installed = await readGameManifest(gameManifestPath);
@@ -14216,35 +14842,35 @@ async function runAnims(opts) {
14216
14842
  }
14217
14843
  }
14218
14844
  const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
14219
- const cacheDir = path14.join(
14845
+ const cacheDir = path15.join(
14220
14846
  opts.cacheDir ?? getAnimsCacheDir(),
14221
14847
  `${manifest.library}-v${manifest.version}`
14222
14848
  );
14223
- await fs14.mkdir(cacheDir, { recursive: true });
14224
- await fs14.mkdir(destDir, { recursive: true });
14849
+ await fs15.mkdir(cacheDir, { recursive: true });
14850
+ await fs15.mkdir(destDir, { recursive: true });
14225
14851
  const base = getAnimsBase(opts.animsBase);
14226
14852
  let installedCount = 0;
14227
14853
  let presentCount = 0;
14228
14854
  let addedBytes = 0;
14229
14855
  const failures = [];
14230
14856
  for (const entry of wanted) {
14231
- const dest = path14.join(destDir, entry.file);
14857
+ const dest = path15.join(destDir, entry.file);
14232
14858
  if (await hasSize(dest, entry.bytes)) {
14233
14859
  presentCount++;
14234
14860
  continue;
14235
14861
  }
14236
14862
  try {
14237
- const cached = path14.join(cacheDir, entry.file);
14863
+ const cached = path15.join(cacheDir, entry.file);
14238
14864
  if (!await hasSize(cached, entry.bytes)) {
14239
14865
  const res = await fetch(base + entry.file);
14240
14866
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
14241
14867
  const buf = Buffer.from(await res.arrayBuffer());
14242
- await fs14.writeFile(cached, buf);
14868
+ await fs15.writeFile(cached, buf);
14243
14869
  }
14244
- await fs14.copyFile(cached, dest);
14870
+ await fs15.copyFile(cached, dest);
14245
14871
  installedCount++;
14246
14872
  addedBytes += entry.bytes;
14247
- log.dim(` ${path14.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
14873
+ log.dim(` ${path15.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
14248
14874
  } catch (err) {
14249
14875
  failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
14250
14876
  }
@@ -14260,13 +14886,13 @@ async function runAnims(opts) {
14260
14886
  version: manifest.version,
14261
14887
  clips: [...union].sort((a, b) => a.localeCompare(b))
14262
14888
  };
14263
- await fs14.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
14889
+ await fs15.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
14264
14890
  log.plain("");
14265
14891
  const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
14266
14892
  if (presentCount > 0) parts.push(`${presentCount} already present`);
14267
14893
  if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
14268
14894
  log.success(
14269
- `${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path14.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
14895
+ `${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path15.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
14270
14896
  );
14271
14897
  for (const [selector, entries] of resolved) {
14272
14898
  const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
@@ -14298,8 +14924,8 @@ async function loadManifest(baseOverride) {
14298
14924
  }
14299
14925
  } catch {
14300
14926
  }
14301
- const snapshotPath = path14.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
14302
- const manifest = JSON.parse(await fs14.readFile(snapshotPath, "utf8"));
14927
+ const snapshotPath = path15.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
14928
+ const manifest = JSON.parse(await fs15.readFile(snapshotPath, "utf8"));
14303
14929
  return { manifest, source: "snapshot" };
14304
14930
  }
14305
14931
  function resolveSelectors(manifest, selectors) {
@@ -14417,21 +15043,21 @@ function printCatalog(log, manifest, selectors) {
14417
15043
  }
14418
15044
  async function readGameManifest(file) {
14419
15045
  try {
14420
- return JSON.parse(await fs14.readFile(file, "utf8"));
15046
+ return JSON.parse(await fs15.readFile(file, "utf8"));
14421
15047
  } catch {
14422
15048
  return null;
14423
15049
  }
14424
15050
  }
14425
15051
  async function hasSize(file, bytes) {
14426
15052
  try {
14427
- return (await fs14.stat(file)).size === bytes;
15053
+ return (await fs15.stat(file)).size === bytes;
14428
15054
  } catch {
14429
15055
  return false;
14430
15056
  }
14431
15057
  }
14432
15058
  async function exists2(p) {
14433
15059
  try {
14434
- await fs14.access(p);
15060
+ await fs15.access(p);
14435
15061
  return true;
14436
15062
  } catch {
14437
15063
  return false;
@@ -14463,6 +15089,7 @@ var TOUCH_KIT = [
14463
15089
  var INPUT_AND_CAMERA = [
14464
15090
  "character/follow-camera.ts",
14465
15091
  "character/aim-cue.ts",
15092
+ "character/first-person.ts",
14466
15093
  "character/keyboard-input.ts",
14467
15094
  "character/touch-joystick.ts",
14468
15095
  ...TOUCH_KIT
@@ -14589,8 +15216,8 @@ var CONTROLLER_FILE_SETS = {
14589
15216
  ]
14590
15217
  }
14591
15218
  };
14592
- var CODE_DEST = path15.join("src", "controllers");
14593
- var ASSETS_DEST = path15.join("public", "assets");
15219
+ var CODE_DEST = path16.join("src", "controllers");
15220
+ var ASSETS_DEST = path16.join("public", "assets");
14594
15221
  async function runController(opts) {
14595
15222
  const log = createLogger({ quiet: opts.quiet });
14596
15223
  if (opts.kind?.trim() === "anims") {
@@ -14607,31 +15234,31 @@ async function runController(opts) {
14607
15234
  process.exitCode = 1;
14608
15235
  return;
14609
15236
  }
14610
- const srcDir = path15.join(getTemplatesDir(), "controllers");
15237
+ const srcDir = path16.join(getTemplatesDir(), "controllers");
14611
15238
  const root = opts.cwd ?? process.cwd();
14612
15239
  const set = CONTROLLER_FILE_SETS[kind];
14613
15240
  log.plain(c.bold(`genex controller ${kind}`));
14614
15241
  log.plain("");
14615
- log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path15.sep)}`);
15242
+ log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path16.sep)}`);
14616
15243
  const plan = [
14617
- ...set.code.map((rel) => ({ from: rel, rel: path15.join(CODE_DEST, rel) })),
15244
+ ...set.code.map((rel) => ({ from: rel, rel: path16.join(CODE_DEST, rel) })),
14618
15245
  ...set.assets.map((rel) => ({
14619
15246
  from: rel,
14620
- rel: path15.join(ASSETS_DEST, path15.basename(rel))
15247
+ rel: path16.join(ASSETS_DEST, path16.basename(rel))
14621
15248
  }))
14622
15249
  ];
14623
15250
  let copied = 0;
14624
15251
  let skipped = 0;
14625
15252
  try {
14626
15253
  for (const file of plan) {
14627
- const dest = path15.join(root, file.rel);
15254
+ const dest = path16.join(root, file.rel);
14628
15255
  if (!opts.force && await exists3(dest)) {
14629
15256
  skipped++;
14630
15257
  log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
14631
15258
  continue;
14632
15259
  }
14633
- await fs15.mkdir(path15.dirname(dest), { recursive: true });
14634
- await fs15.copyFile(path15.join(srcDir, file.from), dest);
15260
+ await fs16.mkdir(path16.dirname(dest), { recursive: true });
15261
+ await fs16.copyFile(path16.join(srcDir, file.from), dest);
14635
15262
  copied++;
14636
15263
  log.dim(` ${file.rel}`);
14637
15264
  }
@@ -14716,9 +15343,9 @@ async function installMeshyCharacterManifest(args) {
14716
15343
  throw new Error("The API returned an invalid Meshy character manifest.");
14717
15344
  }
14718
15345
  assertCompleteMeshyControllerPack(manifest);
14719
- const destination = path15.join(args.root, ASSETS_DEST, "meshy-character.json");
14720
- await fs15.mkdir(path15.dirname(destination), { recursive: true });
14721
- await fs15.writeFile(
15346
+ const destination = path16.join(args.root, ASSETS_DEST, "meshy-character.json");
15347
+ await fs16.mkdir(path16.dirname(destination), { recursive: true });
15348
+ await fs16.writeFile(
14722
15349
  destination,
14723
15350
  `${JSON.stringify(manifest, null, 2)}
14724
15351
  `
@@ -14855,8 +15482,8 @@ function assertCompleteMeshyControllerPack(manifest) {
14855
15482
  }
14856
15483
  async function installOwnerAvatar(args) {
14857
15484
  const { root, srcDir, apiUrl, token, log } = args;
14858
- const dest = path15.join(root, ASSETS_DEST, "avatar.vrm");
14859
- await fs15.mkdir(path15.dirname(dest), { recursive: true });
15485
+ const dest = path16.join(root, ASSETS_DEST, "avatar.vrm");
15486
+ await fs16.mkdir(path16.dirname(dest), { recursive: true });
14860
15487
  if (token) {
14861
15488
  try {
14862
15489
  const res = await apiFetch(`${apiUrl}/api/avatars/me`, {
@@ -14868,7 +15495,7 @@ async function installOwnerAvatar(args) {
14868
15495
  const vrmRes = await fetch(me.vrmUrl);
14869
15496
  if (vrmRes.ok) {
14870
15497
  const buf = Buffer.from(await vrmRes.arrayBuffer());
14871
- await fs15.writeFile(dest, buf);
15498
+ await fs16.writeFile(dest, buf);
14872
15499
  log.dim(` public/assets/avatar.vrm (your avatar \u2014 ${(buf.length / 1e6).toFixed(1)} MB)`);
14873
15500
  return;
14874
15501
  }
@@ -14879,14 +15506,14 @@ async function installOwnerAvatar(args) {
14879
15506
  log.dim(" avatar fetch failed (offline?); using the bundled default.");
14880
15507
  }
14881
15508
  }
14882
- await fs15.copyFile(path15.join(srcDir, "assets", "default-avatar.vrm"), dest);
15509
+ await fs16.copyFile(path16.join(srcDir, "assets", "default-avatar.vrm"), dest);
14883
15510
  log.dim(
14884
15511
  token ? " public/assets/avatar.vrm (bundled default)" : " public/assets/avatar.vrm (bundled default \u2014 sign in and re-run for your own)"
14885
15512
  );
14886
15513
  }
14887
15514
  async function exists3(p) {
14888
15515
  try {
14889
- await fs15.access(p);
15516
+ await fs16.access(p);
14890
15517
  return true;
14891
15518
  } catch {
14892
15519
  return false;
@@ -15482,7 +16109,7 @@ function rank(items, query) {
15482
16109
  }
15483
16110
 
15484
16111
  // src/index.ts
15485
- var GEN_KINDS = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "texture", "image", "video"]);
16112
+ var GEN_KINDS = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "music", "voice", "texture", "image", "video"]);
15486
16113
  var HELP = `${c.bold("genex")} \u2014 set up your project's agent workspace, authorize, and publish 3D games.
15487
16114
 
15488
16115
  ${c.bold("Usage")}
@@ -15497,10 +16124,17 @@ ${c.bold("Usage")}
15497
16124
  genex publish [options] Build + push, then list the game in the gallery.
15498
16125
  genex make-remixable [options] Make THIS game remixable by everyone \u2014 migrates a
15499
16126
  private source onto a public managed genex repo.
15500
- genex model "<prompt>" [options] Generate a 3D model (GLB) into public/assets/models.
15501
- genex skybox "<prompt>" [options] Generate a skybox (equirect) into public/assets/skybox.
15502
- genex sfx "<prompt>" [options] Generate a sound effect (mp3) into public/assets/sfx.
15503
- genex texture "<prompt>" [options] Generate a PBR texture into public/assets/textures.
16127
+ genex model "<prompt>" [options] Generate a 3D model (GLB); prints a public asset URL.
16128
+ genex skybox "<prompt>" [options] Generate a skybox (equirect); prints a public asset URL.
16129
+ genex sfx "<prompt>" [options] Generate a sound effect (mp3); prints a public asset URL.
16130
+ genex music "<prompt>" [options] Generate an instrumental music track (mp3); prints a
16131
+ public asset URL. --duration 10-300s (default 90).
16132
+ genex voice "<line>" [options] Speak a short line (mp3, ElevenLabs TTS); prints a
16133
+ public asset URL. --voice picks a cast archetype
16134
+ (narrator|heroine|gruff|elder|robot|imp; default
16135
+ narrator), --voice-id any raw ElevenLabs voice id.
16136
+ Billed per character, hard-capped at 1000.
16137
+ genex texture "<prompt>" [options] Generate a PBR texture; prints public asset URLs.
15504
16138
  genex image "<prompt>" [options] Generate an image (PNG); prints a public asset URL.
15505
16139
  genex video "<prompt>" [options] Generate a video (mp4); prints a public asset URL.
15506
16140
  genex character "<brief>" Generate 3 character concepts for user selection.
@@ -15511,6 +16145,10 @@ ${c.bold("Usage")}
15511
16145
  Finalize an approved preview as a 10,000-face
15512
16146
  controller-ready Meshy character.
15513
16147
  genex character animate <id> Add Meshy library actions to that character.
16148
+ genex creature "<desc>" One-shot rigged enemy/creature (biped-shaped
16149
+ bodies only \u2014 Meshy rig limit): model \u2192 rig \u2192
16150
+ bind library clips via --animation. No approval
16151
+ ceremony, no player controller pack.
15514
16152
  genex animations search "<intent>" Search Meshy's animation library by gameplay intent.
15515
16153
  genex wait <id> Attach to a generation enqueued with --no-wait and
15516
16154
  print its asset URL(s) when it finishes. Safe to
@@ -15537,10 +16175,19 @@ ${c.bold("Usage")}
15537
16175
  Pure local work (no API); the PNG tools read
15538
16176
  a file or URL, audit scans the project.
15539
16177
 
15540
- ${c.bold("Options for the generators (`model` `skybox` `sfx` `texture` `image` `video`)")}
16178
+ ${c.bold("Options for the generators (`model` `skybox` `sfx` `music` `voice` `texture` `image` `video`)")}
15541
16179
  --terrain (texture) seamless tiling surface for terrain/ground.
15542
- --duration <sec> (sfx, video) target clip length in seconds.
16180
+ --raw (skybox) send the prompt exactly as written \u2014 skip the
16181
+ appended environment-only guard.
16182
+ --duration <sec> (sfx, video, music) target clip length in seconds
16183
+ (music: 10-300, default 90 \u2014 a looping gameplay track).
15543
16184
  --transparent (image) transparent background (alpha) \u2014 for decals/stickers.
16185
+ --glass (image) magenta-key glass lane: generate the panel on a
16186
+ flat #FF00FF screen, then locally solve REAL per-pixel
16187
+ translucency and write RGBA files (glass-1.png\u2026) into
16188
+ --out-dir (default .). For genuinely glassy panels; not
16189
+ for magenta/pink-hued art. Conflicts with --edit/--clean/
16190
+ --upscale/--transparent/--remove-bg/--no-wait.
15544
16191
  --open (image/video) open the finished asset in the user's browser +
15545
16192
  print a link \u2014 show them the concept frame / anything they approve.
15546
16193
  --aspect <ratio> (image) aspect preset (e.g. square, landscape, portrait).
@@ -15552,11 +16199,20 @@ ${c.bold("Options for the generators (`model` `skybox` `sfx` `texture` `image` `
15552
16199
  --edit <url> (image) edit THIS image (asset URL) with the prompt (i2i).
15553
16200
  --clean <url> (image) ML-remove the background of THIS image (asset URL);
15554
16201
  prompt is recorded but unused. Defaults --bg-mode sheet.
16202
+ --upscale <url> (image) upscale THIS image 2x (clarity upscaler); prompt
16203
+ is recorded but unused. ~2 credits class.
16204
+ --inpaint <mask> (image, with --edit) region mask: local PNG path or asset
16205
+ URL; TRANSPARENT hole = the zone to change. Targets WHERE
16206
+ the edit lands (other regions keep their design) but the
16207
+ whole image still re-renders and alpha is destroyed \u2014
16208
+ re-run --clean + re-extract afterwards.
15555
16209
  --remove-bg (image) chain ML background removal after the gen/edit.
15556
- --bg-mode <mode> (image) background-removal model: sprite | glyph | sheet.
16210
+ --bg-mode <mode> (image) background-removal model: sprite | glyph | sheet |
16211
+ matte (BiRefNet soft alpha \u2014 hair/glow/smoke edges).
15557
16212
  --loop (video) generate a seamless loop.
15558
- --resolution <r> (video) 720p | 1080p (default 1080p; use 720p to save
15559
- credits \u2014 loop clips ignore it, model default).
16213
+ --resolution <r> (video) 720p | 1080p (default 1080p; 720p halves the cost
16214
+ when quality genuinely doesn't matter \u2014 loop clips
16215
+ ignore it, model default).
15560
16216
  --frame <url> (video) animate from THIS frame back to itself (seamless loop).
15561
16217
  --first-frame <url> (video) start frame for a two-frame motion \u2026
15562
16218
  --last-frame <url> (video) \u2026 and its end frame.
@@ -15667,11 +16323,14 @@ ${c.bold("Examples")}
15667
16323
  genex model "weathered wooden barrel with iron bands"
15668
16324
  genex skybox "golden hour over a misty mountain range"
15669
16325
  genex sfx "punchy laser zap" --duration 2
16326
+ genex music "brooding orchestral battle loop, seamless loop, no intro or outro" --no-wait
16327
+ genex voice "You shall not pass!" --voice gruff
15670
16328
  genex texture "mossy cracked cobblestone" --terrain
15671
16329
  genex image "retro arcade poster art" --aspect portrait
15672
16330
  genex image "chalk graffiti tag, hand-drawn style" --transparent
15673
16331
  genex video "candle flame flickering in the dark, seamless loop" --loop
15674
16332
  genex image "misty citadel at dusk, cinematic wide shot" --aspect 16:9 --quality high
16333
+ genex image "frosted translucent sci-fi status panel, about 40 percent opacity" --glass --size 1280x832 --quality high
15675
16334
  genex video "slow drifting fog, embers rising" --frame <that-image-url> --duration 8 --no-wait
15676
16335
  genex wait <that-generation-id>
15677
16336
  genex ui extract --in <sheet-url> --out-dir ui/ --names health-bar,minimap,ammo
@@ -15682,6 +16341,7 @@ ${c.bold("Examples")}
15682
16341
  genex character finalize <preview-id> --user-approved --approve-remesh 10000 --animation 466
15683
16342
  genex character "stylized sci-fi courier" --direct-text --no-wait
15684
16343
  genex character animate <character-id> --action <action-id>
16344
+ genex creature "hulking bone seraph, upright stance" --animation <walk-id> --animation <attack-id> --no-wait
15685
16345
  genex controller character --character <character-id>
15686
16346
  genex controller anims sword pistol
15687
16347
  genex controller touch
@@ -15714,7 +16374,11 @@ function parseArgs(argv) {
15714
16374
  "--size",
15715
16375
  "--edit",
15716
16376
  "--clean",
16377
+ "--upscale",
16378
+ "--inpaint",
15717
16379
  "--bg-mode",
16380
+ "--voice",
16381
+ "--voice-id",
15718
16382
  "--frame",
15719
16383
  "--first-frame",
15720
16384
  "--last-frame",
@@ -15788,9 +16452,18 @@ function parseArgs(argv) {
15788
16452
  case "--terrain":
15789
16453
  parsed.options.terrain = true;
15790
16454
  break;
16455
+ case "--raw":
16456
+ parsed.options.raw = true;
16457
+ break;
15791
16458
  case "--transparent":
15792
16459
  parsed.options.transparent = true;
15793
16460
  break;
16461
+ case "--glass":
16462
+ parsed.options.glass = true;
16463
+ break;
16464
+ case "--no-defringe":
16465
+ parsed.options.defringe = false;
16466
+ break;
15794
16467
  case "--loop":
15795
16468
  parsed.options.loop = true;
15796
16469
  break;
@@ -15859,6 +16532,8 @@ function parseArgs(argv) {
15859
16532
  parsed.error = `Unexpected argument: ${arg}`;
15860
16533
  return parsed;
15861
16534
  }
16535
+ } else if (parsed.command === "creature") {
16536
+ parsed.options.name = `${parsed.options.name} ${arg}`;
15862
16537
  } else if (parsed.command === "animations") {
15863
16538
  parsed.options.query = parsed.options.query ? `${parsed.options.query} ${arg}` : arg;
15864
16539
  } else {
@@ -16032,9 +16707,21 @@ function applyValueFlag(options, flag, value) {
16032
16707
  case "--clean":
16033
16708
  options.cleanUrl = value;
16034
16709
  break;
16710
+ case "--upscale":
16711
+ options.upscaleUrl = value;
16712
+ break;
16713
+ case "--inpaint":
16714
+ options.inpaintUrl = value;
16715
+ break;
16716
+ case "--voice":
16717
+ options.voice = value;
16718
+ break;
16719
+ case "--voice-id":
16720
+ options.voiceId = value;
16721
+ break;
16035
16722
  case "--bg-mode":
16036
- if (!["sprite", "glyph", "sheet"].includes(value)) {
16037
- throw new Error(`Invalid --bg-mode value: ${value} (expected sprite|glyph|sheet)`);
16723
+ if (!["sprite", "glyph", "sheet", "matte"].includes(value)) {
16724
+ throw new Error(`Invalid --bg-mode value: ${value} (expected sprite|glyph|sheet|matte)`);
16038
16725
  }
16039
16726
  options.bgMode = value;
16040
16727
  break;
@@ -16155,6 +16842,19 @@ async function main() {
16155
16842
  await runCharacter({ ...parsed.options, prompt: parsed.options.name });
16156
16843
  }
16157
16844
  break;
16845
+ case "creature":
16846
+ if (!parsed.options.json && !parsed.options.quiet) {
16847
+ log.dim(
16848
+ " Biped-shaped creatures only (Meshy rig limit) \u2014 for true quadrupeds/fliers use a static model + the genex-threejs-creatures procedural recipes."
16849
+ );
16850
+ }
16851
+ await runCharacter({
16852
+ ...parsed.options,
16853
+ prompt: parsed.options.name,
16854
+ directText: true,
16855
+ controllerPack: false
16856
+ });
16857
+ break;
16158
16858
  case "animations":
16159
16859
  if (parsed.options.name !== "search") {
16160
16860
  log.error('Unknown animations command. Use `genex animations search "<intent>"`.');