@genex-ai/cli-demo 0.80.2-dev.213 → 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.
- package/dist/index.js +801 -83
- package/package.json +1 -1
- package/templates/controllers/character/first-person.ts +54 -0
- package/templates/controllers/character/follow-camera.ts +35 -1
- package/templates/skills/genex-ai-character/SKILL.md +25 -5
- package/templates/skills/genex-ai-hud/SKILL.md +131 -36
- package/templates/skills/genex-ai-hud/references/masked-fill.md +19 -13
- package/templates/skills/genex-ai-hud/references/stage2-prompt-template.md +2 -2
- package/templates/skills/genex-ai-image/SKILL.md +40 -2
- package/templates/skills/genex-ai-menu/SKILL.md +35 -17
- package/templates/skills/genex-ai-model/SKILL.md +8 -0
- package/templates/skills/genex-ai-music/SKILL.md +142 -0
- package/templates/skills/genex-ai-skybox/SKILL.md +21 -5
- package/templates/skills/genex-ai-video/SKILL.md +13 -7
- package/templates/skills/genex-ai-voice/SKILL.md +151 -0
- package/templates/skills/genex-game-director/SKILL.md +128 -44
- package/templates/skills/genex-game-director/references/design-contract.md +7 -2
- package/templates/skills/genex-game-director/references/routing-map.md +4 -1
- package/templates/skills/genex-threejs-camera-direction/SKILL.md +4 -1
- package/templates/skills/genex-threejs-character-controller/SKILL.md +6 -1
- package/templates/skills/genex-threejs-character-controller/references/wiring.md +13 -2
- package/templates/skills/genex-threejs-creatures/SKILL.md +193 -0
- package/templates/skills/genex-threejs-game-ui/SKILL.md +5 -1
- package/templates/skills/genex-threejs-multiplayer/SKILL.md +6 -1
- package/templates/skills/genex-threejs-touch-controls/SKILL.md +7 -0
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.
|
|
2015
|
-
|
|
2016
|
-
[--
|
|
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).
|
|
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]
|
|
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
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
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
|
|
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
|
-
|
|
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 = [];
|
|
@@ -3006,11 +3316,29 @@ async function walkFiles(dir) {
|
|
|
3006
3316
|
return out;
|
|
3007
3317
|
}
|
|
3008
3318
|
async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
3319
|
+
const viewportFindings = [];
|
|
3320
|
+
try {
|
|
3321
|
+
const indexHtml = await fs10.readFile(path10.join(cwd, "index.html"), "utf8");
|
|
3322
|
+
if (!/<meta[^>]+name=["']viewport["']/i.test(indexHtml)) {
|
|
3323
|
+
viewportFindings.push({
|
|
3324
|
+
kind: "viewport-meta",
|
|
3325
|
+
message: `index.html has no viewport meta \u2014 phones will render a zoomed-out desktop canvas. Add: <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">`
|
|
3326
|
+
});
|
|
3327
|
+
} else if (!/viewport-fit\s*=/i.test(indexHtml)) {
|
|
3328
|
+
viewportFindings.push({
|
|
3329
|
+
kind: "viewport-meta",
|
|
3330
|
+
message: `index.html's viewport meta lacks viewport-fit=cover \u2014 env(safe-area-inset-*) (touch-kit widget offsets) is 0 without it. Append ", viewport-fit=cover" to the meta's content.`
|
|
3331
|
+
});
|
|
3332
|
+
}
|
|
3333
|
+
} catch {
|
|
3334
|
+
}
|
|
3009
3335
|
const absAssets = path10.resolve(cwd, assetDir);
|
|
3010
3336
|
try {
|
|
3011
|
-
if (!(await fs10.stat(absAssets)).isDirectory())
|
|
3337
|
+
if (!(await fs10.stat(absAssets)).isDirectory()) {
|
|
3338
|
+
return viewportFindings.length > 0 ? viewportFindings : null;
|
|
3339
|
+
}
|
|
3012
3340
|
} catch {
|
|
3013
|
-
return null;
|
|
3341
|
+
return viewportFindings.length > 0 ? viewportFindings : null;
|
|
3014
3342
|
}
|
|
3015
3343
|
const srcFiles = (await walkFiles(path10.resolve(cwd, srcDir))).filter(
|
|
3016
3344
|
(p) => AUDIT_SRC_EXTS.has(path10.extname(p).toLowerCase())
|
|
@@ -3030,8 +3358,94 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
3030
3358
|
}
|
|
3031
3359
|
}
|
|
3032
3360
|
const referenced = (basename) => sources.some((s) => s.text.includes(basename));
|
|
3033
|
-
const findings = [];
|
|
3361
|
+
const findings = [...viewportFindings];
|
|
3034
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
|
+
}
|
|
3035
3449
|
const maskReported = /* @__PURE__ */ new Set();
|
|
3036
3450
|
for (const p of assetFiles) {
|
|
3037
3451
|
const m = /^(.+)\.annotated-progress\.json$/.exec(path10.basename(p));
|
|
@@ -3095,11 +3509,14 @@ async function uiAudit(opts, log) {
|
|
|
3095
3509
|
log.success("HUD wiring audit clean \u2014 every asset referenced, no naive %-fill patterns.");
|
|
3096
3510
|
return;
|
|
3097
3511
|
}
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
);
|
|
3101
|
-
|
|
3102
|
-
|
|
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;
|
|
3103
3520
|
}
|
|
3104
3521
|
|
|
3105
3522
|
// src/lib/deploy.ts
|
|
@@ -3943,7 +4360,7 @@ async function detectSurfaceScan(cwd = process.cwd()) {
|
|
|
3943
4360
|
}
|
|
3944
4361
|
return found;
|
|
3945
4362
|
}
|
|
3946
|
-
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"]);
|
|
3947
4364
|
var UNPICKED_AFTER_MS = 15 * 60 * 1e3;
|
|
3948
4365
|
async function detectGenerationAudit(cwd = process.cwd()) {
|
|
3949
4366
|
const ledger = await readLedger(cwd);
|
|
@@ -4267,7 +4684,86 @@ async function runPreview(opts) {
|
|
|
4267
4684
|
}
|
|
4268
4685
|
|
|
4269
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
|
|
4270
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
|
+
}
|
|
4271
4767
|
|
|
4272
4768
|
// src/lib/open.ts
|
|
4273
4769
|
import { spawn as spawn4 } from "child_process";
|
|
@@ -4348,10 +4844,22 @@ async function* readSSE(body) {
|
|
|
4348
4844
|
|
|
4349
4845
|
// src/commands/generate.ts
|
|
4350
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
|
+
}
|
|
4351
4855
|
function buildGenOptions(kind, opts) {
|
|
4352
4856
|
const options = { ...opts.generationOptions };
|
|
4353
4857
|
if (kind === "texture" && opts.terrain) options.terrain = true;
|
|
4354
|
-
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
|
+
}
|
|
4355
4863
|
if (kind === "image") {
|
|
4356
4864
|
if (opts.transparent) options.transparent = true;
|
|
4357
4865
|
if (opts.aspect) options.aspect = opts.aspect;
|
|
@@ -4370,6 +4878,11 @@ function buildGenOptions(kind, opts) {
|
|
|
4370
4878
|
options.op = "remove-bg";
|
|
4371
4879
|
options.bgMode = opts.bgMode ?? "sheet";
|
|
4372
4880
|
}
|
|
4881
|
+
if (opts.upscaleUrl) {
|
|
4882
|
+
options.sourceUrl = opts.upscaleUrl;
|
|
4883
|
+
options.op = "upscale";
|
|
4884
|
+
}
|
|
4885
|
+
if (opts.inpaintUrl) options.maskUrl = opts.inpaintUrl;
|
|
4373
4886
|
if (opts.removeBackground) options.removeBackground = true;
|
|
4374
4887
|
if (opts.bgMode) options.bgMode = opts.bgMode;
|
|
4375
4888
|
}
|
|
@@ -4385,12 +4898,45 @@ function buildGenOptions(kind, opts) {
|
|
|
4385
4898
|
}
|
|
4386
4899
|
async function runGenerate(kind, opts) {
|
|
4387
4900
|
const log = createLogger({ quiet: opts.quiet || opts.json });
|
|
4388
|
-
const
|
|
4389
|
-
if (!
|
|
4901
|
+
const typedPrompt = opts.prompt?.trim();
|
|
4902
|
+
if (!typedPrompt) {
|
|
4390
4903
|
log.error(`Missing prompt. Usage: ${c.cyan(`genex ${kind} "<prompt>"`)}`);
|
|
4391
4904
|
process.exitCode = 1;
|
|
4392
4905
|
return;
|
|
4393
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
|
+
}
|
|
4394
4940
|
const token = opts.token ?? await readUserToken(opts.envPath);
|
|
4395
4941
|
if (!token) {
|
|
4396
4942
|
log.error("Not authorized. Run `genex init` first to sign in.");
|
|
@@ -4402,6 +4948,7 @@ async function runGenerate(kind, opts) {
|
|
|
4402
4948
|
const options = buildGenOptions(kind, opts);
|
|
4403
4949
|
log.plain(c.bold(`genex ${kind}`));
|
|
4404
4950
|
log.dim(` ${prompt}`);
|
|
4951
|
+
if (skyboxGuard?.guarded) log.dim(" \u21B3 environment-only guard applied (--raw to disable)");
|
|
4405
4952
|
log.plain("");
|
|
4406
4953
|
let id;
|
|
4407
4954
|
try {
|
|
@@ -4454,9 +5001,104 @@ async function runGenerate(kind, opts) {
|
|
|
4454
5001
|
}
|
|
4455
5002
|
return id;
|
|
4456
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
|
+
}
|
|
4457
5023
|
await awaitAndReport(apiUrl, token, id, kind, log, opts.open, opts.json);
|
|
4458
5024
|
return id;
|
|
4459
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
|
+
}
|
|
4460
5102
|
async function awaitAndReport(apiUrl, token, id, kind, log, open = false, json = false) {
|
|
4461
5103
|
if (!json) log.step("Generating\u2026 (this can take up to a minute)");
|
|
4462
5104
|
const view = await waitForTerminalView(
|
|
@@ -4640,7 +5282,7 @@ async function reportTextureSeam(url, log) {
|
|
|
4640
5282
|
try {
|
|
4641
5283
|
const res = await fetch(url);
|
|
4642
5284
|
if (!res.ok) return;
|
|
4643
|
-
const png =
|
|
5285
|
+
const png = PNG4.sync.read(Buffer.from(await res.arrayBuffer()));
|
|
4644
5286
|
if (png.width < 4 || png.height < 4) return;
|
|
4645
5287
|
const r = measureSeam(png);
|
|
4646
5288
|
if (r.seamless) {
|
|
@@ -4664,6 +5306,8 @@ function printHint(kind, view, files, log) {
|
|
|
4664
5306
|
model: `Standard GLB \u2014 load with GLTFLoader straight from the URL \u2014 see the genex-ai-model skill. url = "${url}"`,
|
|
4665
5307
|
skybox: `Load as an equirectangular texture \u2192 scene.background + scene.environment \u2014 see genex-ai-skybox. url = "${url}"`,
|
|
4666
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}"`,
|
|
4667
5311
|
texture: `Load each map with TextureLoader (RepeatWrapping) into a MeshStandardMaterial \u2014 see genex-ai-texture. Use the URLs above by role.`,
|
|
4668
5312
|
image: `Load with TextureLoader (set colorSpace = SRGBColorSpace) onto any mesh/plane/sprite \u2014 see genex-ai-image. url = "${url}"`,
|
|
4669
5313
|
video: `Wire an HTMLVideoElement (crossOrigin="anonymous", muted, loop, playsInline) into a THREE.VideoTexture \u2014 see genex-ai-video. url = "${url}"`,
|
|
@@ -4998,8 +5642,8 @@ async function toRow(e, v, cwd) {
|
|
|
4998
5642
|
}
|
|
4999
5643
|
|
|
5000
5644
|
// src/commands/controller.ts
|
|
5001
|
-
import
|
|
5002
|
-
import
|
|
5645
|
+
import fs16 from "fs/promises";
|
|
5646
|
+
import path16 from "path";
|
|
5003
5647
|
|
|
5004
5648
|
// ../../packages/meshy-animation-catalog/src/index.ts
|
|
5005
5649
|
import { createHash } from "crypto";
|
|
@@ -14129,9 +14773,9 @@ function searchMeshyAnimations(query, options = {}) {
|
|
|
14129
14773
|
}
|
|
14130
14774
|
|
|
14131
14775
|
// src/lib/anims.ts
|
|
14132
|
-
import
|
|
14133
|
-
import
|
|
14134
|
-
var ANIMS_DEST =
|
|
14776
|
+
import fs15 from "fs/promises";
|
|
14777
|
+
import path15 from "path";
|
|
14778
|
+
var ANIMS_DEST = path15.join("public", "assets", "anims");
|
|
14135
14779
|
var HIDDEN_TAG = "reference";
|
|
14136
14780
|
async function runAnims(opts) {
|
|
14137
14781
|
const log = createLogger({ quiet: opts.quiet });
|
|
@@ -14147,7 +14791,7 @@ async function runAnims(opts) {
|
|
|
14147
14791
|
printCatalog(log, manifest, selectors);
|
|
14148
14792
|
return;
|
|
14149
14793
|
}
|
|
14150
|
-
const controllerMarker =
|
|
14794
|
+
const controllerMarker = path15.join(root, "src", "controllers", "character");
|
|
14151
14795
|
if (!await exists2(controllerMarker)) {
|
|
14152
14796
|
log.error(
|
|
14153
14797
|
`No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
|
|
@@ -14156,11 +14800,11 @@ async function runAnims(opts) {
|
|
|
14156
14800
|
process.exitCode = 1;
|
|
14157
14801
|
return;
|
|
14158
14802
|
}
|
|
14159
|
-
const destDir =
|
|
14160
|
-
const gameManifestPath =
|
|
14803
|
+
const destDir = path15.join(root, ANIMS_DEST);
|
|
14804
|
+
const gameManifestPath = path15.join(destDir, "manifest.json");
|
|
14161
14805
|
if (opts.reset) {
|
|
14162
|
-
await
|
|
14163
|
-
log.step(`Cleared ${c.cyan(ANIMS_DEST +
|
|
14806
|
+
await fs15.rm(destDir, { recursive: true, force: true });
|
|
14807
|
+
log.step(`Cleared ${c.cyan(ANIMS_DEST + path15.sep)} (--reset)`);
|
|
14164
14808
|
}
|
|
14165
14809
|
if (selectors.length === 0) {
|
|
14166
14810
|
const installed = await readGameManifest(gameManifestPath);
|
|
@@ -14198,35 +14842,35 @@ async function runAnims(opts) {
|
|
|
14198
14842
|
}
|
|
14199
14843
|
}
|
|
14200
14844
|
const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
14201
|
-
const cacheDir =
|
|
14845
|
+
const cacheDir = path15.join(
|
|
14202
14846
|
opts.cacheDir ?? getAnimsCacheDir(),
|
|
14203
14847
|
`${manifest.library}-v${manifest.version}`
|
|
14204
14848
|
);
|
|
14205
|
-
await
|
|
14206
|
-
await
|
|
14849
|
+
await fs15.mkdir(cacheDir, { recursive: true });
|
|
14850
|
+
await fs15.mkdir(destDir, { recursive: true });
|
|
14207
14851
|
const base = getAnimsBase(opts.animsBase);
|
|
14208
14852
|
let installedCount = 0;
|
|
14209
14853
|
let presentCount = 0;
|
|
14210
14854
|
let addedBytes = 0;
|
|
14211
14855
|
const failures = [];
|
|
14212
14856
|
for (const entry of wanted) {
|
|
14213
|
-
const dest =
|
|
14857
|
+
const dest = path15.join(destDir, entry.file);
|
|
14214
14858
|
if (await hasSize(dest, entry.bytes)) {
|
|
14215
14859
|
presentCount++;
|
|
14216
14860
|
continue;
|
|
14217
14861
|
}
|
|
14218
14862
|
try {
|
|
14219
|
-
const cached =
|
|
14863
|
+
const cached = path15.join(cacheDir, entry.file);
|
|
14220
14864
|
if (!await hasSize(cached, entry.bytes)) {
|
|
14221
14865
|
const res = await fetch(base + entry.file);
|
|
14222
14866
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
14223
14867
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
14224
|
-
await
|
|
14868
|
+
await fs15.writeFile(cached, buf);
|
|
14225
14869
|
}
|
|
14226
|
-
await
|
|
14870
|
+
await fs15.copyFile(cached, dest);
|
|
14227
14871
|
installedCount++;
|
|
14228
14872
|
addedBytes += entry.bytes;
|
|
14229
|
-
log.dim(` ${
|
|
14873
|
+
log.dim(` ${path15.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
|
|
14230
14874
|
} catch (err) {
|
|
14231
14875
|
failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
|
|
14232
14876
|
}
|
|
@@ -14242,13 +14886,13 @@ async function runAnims(opts) {
|
|
|
14242
14886
|
version: manifest.version,
|
|
14243
14887
|
clips: [...union].sort((a, b) => a.localeCompare(b))
|
|
14244
14888
|
};
|
|
14245
|
-
await
|
|
14889
|
+
await fs15.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
|
|
14246
14890
|
log.plain("");
|
|
14247
14891
|
const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
|
|
14248
14892
|
if (presentCount > 0) parts.push(`${presentCount} already present`);
|
|
14249
14893
|
if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
|
|
14250
14894
|
log.success(
|
|
14251
|
-
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST +
|
|
14895
|
+
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path15.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
|
|
14252
14896
|
);
|
|
14253
14897
|
for (const [selector, entries] of resolved) {
|
|
14254
14898
|
const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
|
|
@@ -14280,8 +14924,8 @@ async function loadManifest(baseOverride) {
|
|
|
14280
14924
|
}
|
|
14281
14925
|
} catch {
|
|
14282
14926
|
}
|
|
14283
|
-
const snapshotPath =
|
|
14284
|
-
const manifest = JSON.parse(await
|
|
14927
|
+
const snapshotPath = path15.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
|
|
14928
|
+
const manifest = JSON.parse(await fs15.readFile(snapshotPath, "utf8"));
|
|
14285
14929
|
return { manifest, source: "snapshot" };
|
|
14286
14930
|
}
|
|
14287
14931
|
function resolveSelectors(manifest, selectors) {
|
|
@@ -14399,21 +15043,21 @@ function printCatalog(log, manifest, selectors) {
|
|
|
14399
15043
|
}
|
|
14400
15044
|
async function readGameManifest(file) {
|
|
14401
15045
|
try {
|
|
14402
|
-
return JSON.parse(await
|
|
15046
|
+
return JSON.parse(await fs15.readFile(file, "utf8"));
|
|
14403
15047
|
} catch {
|
|
14404
15048
|
return null;
|
|
14405
15049
|
}
|
|
14406
15050
|
}
|
|
14407
15051
|
async function hasSize(file, bytes) {
|
|
14408
15052
|
try {
|
|
14409
|
-
return (await
|
|
15053
|
+
return (await fs15.stat(file)).size === bytes;
|
|
14410
15054
|
} catch {
|
|
14411
15055
|
return false;
|
|
14412
15056
|
}
|
|
14413
15057
|
}
|
|
14414
15058
|
async function exists2(p) {
|
|
14415
15059
|
try {
|
|
14416
|
-
await
|
|
15060
|
+
await fs15.access(p);
|
|
14417
15061
|
return true;
|
|
14418
15062
|
} catch {
|
|
14419
15063
|
return false;
|
|
@@ -14445,6 +15089,7 @@ var TOUCH_KIT = [
|
|
|
14445
15089
|
var INPUT_AND_CAMERA = [
|
|
14446
15090
|
"character/follow-camera.ts",
|
|
14447
15091
|
"character/aim-cue.ts",
|
|
15092
|
+
"character/first-person.ts",
|
|
14448
15093
|
"character/keyboard-input.ts",
|
|
14449
15094
|
"character/touch-joystick.ts",
|
|
14450
15095
|
...TOUCH_KIT
|
|
@@ -14571,8 +15216,8 @@ var CONTROLLER_FILE_SETS = {
|
|
|
14571
15216
|
]
|
|
14572
15217
|
}
|
|
14573
15218
|
};
|
|
14574
|
-
var CODE_DEST =
|
|
14575
|
-
var ASSETS_DEST =
|
|
15219
|
+
var CODE_DEST = path16.join("src", "controllers");
|
|
15220
|
+
var ASSETS_DEST = path16.join("public", "assets");
|
|
14576
15221
|
async function runController(opts) {
|
|
14577
15222
|
const log = createLogger({ quiet: opts.quiet });
|
|
14578
15223
|
if (opts.kind?.trim() === "anims") {
|
|
@@ -14589,31 +15234,31 @@ async function runController(opts) {
|
|
|
14589
15234
|
process.exitCode = 1;
|
|
14590
15235
|
return;
|
|
14591
15236
|
}
|
|
14592
|
-
const srcDir =
|
|
15237
|
+
const srcDir = path16.join(getTemplatesDir(), "controllers");
|
|
14593
15238
|
const root = opts.cwd ?? process.cwd();
|
|
14594
15239
|
const set = CONTROLLER_FILE_SETS[kind];
|
|
14595
15240
|
log.plain(c.bold(`genex controller ${kind}`));
|
|
14596
15241
|
log.plain("");
|
|
14597
|
-
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST +
|
|
15242
|
+
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path16.sep)}`);
|
|
14598
15243
|
const plan = [
|
|
14599
|
-
...set.code.map((rel) => ({ from: rel, rel:
|
|
15244
|
+
...set.code.map((rel) => ({ from: rel, rel: path16.join(CODE_DEST, rel) })),
|
|
14600
15245
|
...set.assets.map((rel) => ({
|
|
14601
15246
|
from: rel,
|
|
14602
|
-
rel:
|
|
15247
|
+
rel: path16.join(ASSETS_DEST, path16.basename(rel))
|
|
14603
15248
|
}))
|
|
14604
15249
|
];
|
|
14605
15250
|
let copied = 0;
|
|
14606
15251
|
let skipped = 0;
|
|
14607
15252
|
try {
|
|
14608
15253
|
for (const file of plan) {
|
|
14609
|
-
const dest =
|
|
15254
|
+
const dest = path16.join(root, file.rel);
|
|
14610
15255
|
if (!opts.force && await exists3(dest)) {
|
|
14611
15256
|
skipped++;
|
|
14612
15257
|
log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
|
|
14613
15258
|
continue;
|
|
14614
15259
|
}
|
|
14615
|
-
await
|
|
14616
|
-
await
|
|
15260
|
+
await fs16.mkdir(path16.dirname(dest), { recursive: true });
|
|
15261
|
+
await fs16.copyFile(path16.join(srcDir, file.from), dest);
|
|
14617
15262
|
copied++;
|
|
14618
15263
|
log.dim(` ${file.rel}`);
|
|
14619
15264
|
}
|
|
@@ -14698,9 +15343,9 @@ async function installMeshyCharacterManifest(args) {
|
|
|
14698
15343
|
throw new Error("The API returned an invalid Meshy character manifest.");
|
|
14699
15344
|
}
|
|
14700
15345
|
assertCompleteMeshyControllerPack(manifest);
|
|
14701
|
-
const destination =
|
|
14702
|
-
await
|
|
14703
|
-
await
|
|
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(
|
|
14704
15349
|
destination,
|
|
14705
15350
|
`${JSON.stringify(manifest, null, 2)}
|
|
14706
15351
|
`
|
|
@@ -14837,8 +15482,8 @@ function assertCompleteMeshyControllerPack(manifest) {
|
|
|
14837
15482
|
}
|
|
14838
15483
|
async function installOwnerAvatar(args) {
|
|
14839
15484
|
const { root, srcDir, apiUrl, token, log } = args;
|
|
14840
|
-
const dest =
|
|
14841
|
-
await
|
|
15485
|
+
const dest = path16.join(root, ASSETS_DEST, "avatar.vrm");
|
|
15486
|
+
await fs16.mkdir(path16.dirname(dest), { recursive: true });
|
|
14842
15487
|
if (token) {
|
|
14843
15488
|
try {
|
|
14844
15489
|
const res = await apiFetch(`${apiUrl}/api/avatars/me`, {
|
|
@@ -14850,7 +15495,7 @@ async function installOwnerAvatar(args) {
|
|
|
14850
15495
|
const vrmRes = await fetch(me.vrmUrl);
|
|
14851
15496
|
if (vrmRes.ok) {
|
|
14852
15497
|
const buf = Buffer.from(await vrmRes.arrayBuffer());
|
|
14853
|
-
await
|
|
15498
|
+
await fs16.writeFile(dest, buf);
|
|
14854
15499
|
log.dim(` public/assets/avatar.vrm (your avatar \u2014 ${(buf.length / 1e6).toFixed(1)} MB)`);
|
|
14855
15500
|
return;
|
|
14856
15501
|
}
|
|
@@ -14861,14 +15506,14 @@ async function installOwnerAvatar(args) {
|
|
|
14861
15506
|
log.dim(" avatar fetch failed (offline?); using the bundled default.");
|
|
14862
15507
|
}
|
|
14863
15508
|
}
|
|
14864
|
-
await
|
|
15509
|
+
await fs16.copyFile(path16.join(srcDir, "assets", "default-avatar.vrm"), dest);
|
|
14865
15510
|
log.dim(
|
|
14866
15511
|
token ? " public/assets/avatar.vrm (bundled default)" : " public/assets/avatar.vrm (bundled default \u2014 sign in and re-run for your own)"
|
|
14867
15512
|
);
|
|
14868
15513
|
}
|
|
14869
15514
|
async function exists3(p) {
|
|
14870
15515
|
try {
|
|
14871
|
-
await
|
|
15516
|
+
await fs16.access(p);
|
|
14872
15517
|
return true;
|
|
14873
15518
|
} catch {
|
|
14874
15519
|
return false;
|
|
@@ -15464,7 +16109,7 @@ function rank(items, query) {
|
|
|
15464
16109
|
}
|
|
15465
16110
|
|
|
15466
16111
|
// src/index.ts
|
|
15467
|
-
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"]);
|
|
15468
16113
|
var HELP = `${c.bold("genex")} \u2014 set up your project's agent workspace, authorize, and publish 3D games.
|
|
15469
16114
|
|
|
15470
16115
|
${c.bold("Usage")}
|
|
@@ -15479,10 +16124,17 @@ ${c.bold("Usage")}
|
|
|
15479
16124
|
genex publish [options] Build + push, then list the game in the gallery.
|
|
15480
16125
|
genex make-remixable [options] Make THIS game remixable by everyone \u2014 migrates a
|
|
15481
16126
|
private source onto a public managed genex repo.
|
|
15482
|
-
genex model "<prompt>" [options] Generate a 3D model (GLB)
|
|
15483
|
-
genex skybox "<prompt>" [options] Generate a skybox (equirect)
|
|
15484
|
-
genex sfx "<prompt>" [options] Generate a sound effect (mp3)
|
|
15485
|
-
genex
|
|
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.
|
|
15486
16138
|
genex image "<prompt>" [options] Generate an image (PNG); prints a public asset URL.
|
|
15487
16139
|
genex video "<prompt>" [options] Generate a video (mp4); prints a public asset URL.
|
|
15488
16140
|
genex character "<brief>" Generate 3 character concepts for user selection.
|
|
@@ -15493,6 +16145,10 @@ ${c.bold("Usage")}
|
|
|
15493
16145
|
Finalize an approved preview as a 10,000-face
|
|
15494
16146
|
controller-ready Meshy character.
|
|
15495
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.
|
|
15496
16152
|
genex animations search "<intent>" Search Meshy's animation library by gameplay intent.
|
|
15497
16153
|
genex wait <id> Attach to a generation enqueued with --no-wait and
|
|
15498
16154
|
print its asset URL(s) when it finishes. Safe to
|
|
@@ -15519,10 +16175,19 @@ ${c.bold("Usage")}
|
|
|
15519
16175
|
Pure local work (no API); the PNG tools read
|
|
15520
16176
|
a file or URL, audit scans the project.
|
|
15521
16177
|
|
|
15522
|
-
${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`)")}
|
|
15523
16179
|
--terrain (texture) seamless tiling surface for terrain/ground.
|
|
15524
|
-
--
|
|
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).
|
|
15525
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.
|
|
15526
16191
|
--open (image/video) open the finished asset in the user's browser +
|
|
15527
16192
|
print a link \u2014 show them the concept frame / anything they approve.
|
|
15528
16193
|
--aspect <ratio> (image) aspect preset (e.g. square, landscape, portrait).
|
|
@@ -15534,11 +16199,20 @@ ${c.bold("Options for the generators (`model` `skybox` `sfx` `texture` `image` `
|
|
|
15534
16199
|
--edit <url> (image) edit THIS image (asset URL) with the prompt (i2i).
|
|
15535
16200
|
--clean <url> (image) ML-remove the background of THIS image (asset URL);
|
|
15536
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.
|
|
15537
16209
|
--remove-bg (image) chain ML background removal after the gen/edit.
|
|
15538
|
-
--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).
|
|
15539
16212
|
--loop (video) generate a seamless loop.
|
|
15540
|
-
--resolution <r> (video) 720p | 1080p (default 1080p;
|
|
15541
|
-
|
|
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).
|
|
15542
16216
|
--frame <url> (video) animate from THIS frame back to itself (seamless loop).
|
|
15543
16217
|
--first-frame <url> (video) start frame for a two-frame motion \u2026
|
|
15544
16218
|
--last-frame <url> (video) \u2026 and its end frame.
|
|
@@ -15649,11 +16323,14 @@ ${c.bold("Examples")}
|
|
|
15649
16323
|
genex model "weathered wooden barrel with iron bands"
|
|
15650
16324
|
genex skybox "golden hour over a misty mountain range"
|
|
15651
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
|
|
15652
16328
|
genex texture "mossy cracked cobblestone" --terrain
|
|
15653
16329
|
genex image "retro arcade poster art" --aspect portrait
|
|
15654
16330
|
genex image "chalk graffiti tag, hand-drawn style" --transparent
|
|
15655
16331
|
genex video "candle flame flickering in the dark, seamless loop" --loop
|
|
15656
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
|
|
15657
16334
|
genex video "slow drifting fog, embers rising" --frame <that-image-url> --duration 8 --no-wait
|
|
15658
16335
|
genex wait <that-generation-id>
|
|
15659
16336
|
genex ui extract --in <sheet-url> --out-dir ui/ --names health-bar,minimap,ammo
|
|
@@ -15664,6 +16341,7 @@ ${c.bold("Examples")}
|
|
|
15664
16341
|
genex character finalize <preview-id> --user-approved --approve-remesh 10000 --animation 466
|
|
15665
16342
|
genex character "stylized sci-fi courier" --direct-text --no-wait
|
|
15666
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
|
|
15667
16345
|
genex controller character --character <character-id>
|
|
15668
16346
|
genex controller anims sword pistol
|
|
15669
16347
|
genex controller touch
|
|
@@ -15696,7 +16374,11 @@ function parseArgs(argv) {
|
|
|
15696
16374
|
"--size",
|
|
15697
16375
|
"--edit",
|
|
15698
16376
|
"--clean",
|
|
16377
|
+
"--upscale",
|
|
16378
|
+
"--inpaint",
|
|
15699
16379
|
"--bg-mode",
|
|
16380
|
+
"--voice",
|
|
16381
|
+
"--voice-id",
|
|
15700
16382
|
"--frame",
|
|
15701
16383
|
"--first-frame",
|
|
15702
16384
|
"--last-frame",
|
|
@@ -15770,9 +16452,18 @@ function parseArgs(argv) {
|
|
|
15770
16452
|
case "--terrain":
|
|
15771
16453
|
parsed.options.terrain = true;
|
|
15772
16454
|
break;
|
|
16455
|
+
case "--raw":
|
|
16456
|
+
parsed.options.raw = true;
|
|
16457
|
+
break;
|
|
15773
16458
|
case "--transparent":
|
|
15774
16459
|
parsed.options.transparent = true;
|
|
15775
16460
|
break;
|
|
16461
|
+
case "--glass":
|
|
16462
|
+
parsed.options.glass = true;
|
|
16463
|
+
break;
|
|
16464
|
+
case "--no-defringe":
|
|
16465
|
+
parsed.options.defringe = false;
|
|
16466
|
+
break;
|
|
15776
16467
|
case "--loop":
|
|
15777
16468
|
parsed.options.loop = true;
|
|
15778
16469
|
break;
|
|
@@ -15841,6 +16532,8 @@ function parseArgs(argv) {
|
|
|
15841
16532
|
parsed.error = `Unexpected argument: ${arg}`;
|
|
15842
16533
|
return parsed;
|
|
15843
16534
|
}
|
|
16535
|
+
} else if (parsed.command === "creature") {
|
|
16536
|
+
parsed.options.name = `${parsed.options.name} ${arg}`;
|
|
15844
16537
|
} else if (parsed.command === "animations") {
|
|
15845
16538
|
parsed.options.query = parsed.options.query ? `${parsed.options.query} ${arg}` : arg;
|
|
15846
16539
|
} else {
|
|
@@ -16014,9 +16707,21 @@ function applyValueFlag(options, flag, value) {
|
|
|
16014
16707
|
case "--clean":
|
|
16015
16708
|
options.cleanUrl = value;
|
|
16016
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;
|
|
16017
16722
|
case "--bg-mode":
|
|
16018
|
-
if (!["sprite", "glyph", "sheet"].includes(value)) {
|
|
16019
|
-
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)`);
|
|
16020
16725
|
}
|
|
16021
16726
|
options.bgMode = value;
|
|
16022
16727
|
break;
|
|
@@ -16137,6 +16842,19 @@ async function main() {
|
|
|
16137
16842
|
await runCharacter({ ...parsed.options, prompt: parsed.options.name });
|
|
16138
16843
|
}
|
|
16139
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;
|
|
16140
16858
|
case "animations":
|
|
16141
16859
|
if (parsed.options.name !== "search") {
|
|
16142
16860
|
log.error('Unknown animations command. Use `genex animations search "<intent>"`.');
|