@genex-ai/cli-demo 0.90.0-dev.227 → 0.92.0-dev.229
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/README.md +4 -1
- package/dist/index.js +598 -91
- package/package.json +1 -1
- package/templates/controllers/NOTICE.md +6 -4
- package/templates/controllers/character/player-character.ts +217 -0
- package/templates/skills/genex-ai-character/SKILL.md +91 -25
- package/templates/skills/genex-ai-character/references/motion-generation.md +125 -0
- package/templates/skills/genex-ai-image/SKILL.md +5 -1
- package/templates/skills/genex-ai-menu/SKILL.md +14 -6
- package/templates/skills/genex-ai-skybox/SKILL.md +19 -6
- package/templates/skills/genex-game-director/SKILL.md +29 -11
- package/templates/skills/genex-game-director/references/routing-map.md +4 -3
- package/templates/skills/genex-threejs-character-controller/SKILL.md +105 -52
- package/templates/skills/genex-threejs-character-controller/references/animations.md +62 -50
- package/templates/skills/genex-threejs-creatures/SKILL.md +14 -4
- package/templates/skills/genex-threejs-embed-auth/SKILL.md +5 -3
- package/templates/skills/genex-threejs-multiplayer/SKILL.md +10 -8
- package/templates/skills/genex-threejs-multiplayer/references/host-physics.md +8 -7
- package/templates/skills/genex-threejs-procedural-animation/SKILL.md +2 -2
- package/templates/skills/genex-ai-character/references/motion.md +0 -145
package/dist/index.js
CHANGED
|
@@ -252,7 +252,7 @@ var CONTRACT_END = "<!-- genex:contract:end -->";
|
|
|
252
252
|
var GENEX_CONTRACT_BLOCK = `${CONTRACT_BEGIN}
|
|
253
253
|
# Genex build contract (always in effect for this game)
|
|
254
254
|
|
|
255
|
-
Your capabilities (all via \`npx genex \u2026\`): generate \`model\` \xB7 \`skybox\` \xB7 \`sfx\` \xB7 \`music\` \xB7 \`voice\` \xB7 \`texture\` \xB7 \`image\` (\`--edit\` \xB7 \`--inpaint\` \xB7 \`--glass\` \xB7 \`--clean\` \xB7 \`--upscale\`) \xB7 \`video\` \xB7 rigged \`character\` / \`creature\` \xB7 the pixel toolbox \`ui extract|masks|plate|text-color|trim|audit\` \xB7 vendored \`controller character|car|drone|touch|quality\` \xB7 \`animations search\` \xB7 \`wait <id>\` / \`wait --all\` \xB7 \`preview\` / \`publish\`. Full options: \`npx genex --help\`. Task\u2192lane routing lives in the \`genex-game-director\` skill's routing map \u2014 re-load it whenever you're unsure which lane owns a task.
|
|
255
|
+
Your capabilities (all via \`npx genex \u2026\`): generate \`model\` \xB7 \`skybox\` \xB7 \`sfx\` \xB7 \`music\` \xB7 \`voice\` \xB7 \`texture\` \xB7 \`image\` (\`--edit\` \xB7 \`--inpaint\` \xB7 \`--glass\` \xB7 \`--clean\` \xB7 \`--upscale\`) \xB7 \`video\` \xB7 rigged \`character\` / \`creature\` \xB7 \`character animate <id> "<verb>"\` (also \`creature animate\`; \`--locomotion\` for the 8-way movement set, \`--video\` for your own footage) \xB7 the pixel toolbox \`ui extract|masks|plate|text-color|trim|audit\` \xB7 vendored \`controller character|car|drone|touch|quality\` \xB7 \`animations search\` \xB7 \`wait <id>\` / \`wait --all\` \xB7 \`preview\` / \`publish\`. Full options: \`npx genex --help\`. Task\u2192lane routing lives in the \`genex-game-director\` skill's routing map \u2014 re-load it whenever you're unsure which lane owns a task.
|
|
256
256
|
|
|
257
257
|
1. Load the \`genex-game-director\` skill and follow it end to end before any game code. After ANY context compaction or session resume, re-read this file and \`DESIGN.md\`, re-load the skill for the stage you are executing, and continue from the Build plan's \`Now:\` line \u2014 never from memory alone.
|
|
258
258
|
2. The design interview always happens: 2\u20134 build-forking questions, ONE round, before DESIGN.md is written \u2014 even when the request already names the game (then it confirms the pitch). Ask with your built-in question tool with clickable answer options when you have one; a short numbered list in plain chat otherwise. Only after asking may you proceed on stated assumptions if the player stays silent; record each as "assumed \u2014 player didn't answer" in DESIGN.md \u2192 Decisions.
|
|
@@ -265,13 +265,16 @@ Your capabilities (all via \`npx genex \u2026\`): generate \`model\` \xB7 \`skyb
|
|
|
265
265
|
9. Never draw a rectangular backing plate behind bars, digits, or icons \u2014 in sprites or CSS. Ornament lives on the widget's own silhouette; a truly needed shaped plate comes from \`npx genex ui plate\`.
|
|
266
266
|
10. Fonts: the brief's display + body pair comes from the menu skill's genre table (or carries a one-line stated reason) and is LOADED for real in \`index.html\`.
|
|
267
267
|
11. Never park ready work behind a question, and never stall on an unanswered one \u2014 decide, state the decision in chat, record it, keep building.
|
|
268
|
-
12. Wire-in floors before any publish and before ending a session: sprite HUD wired (or the recorded CSS lane finished), menu video playing (or its recorded still fallback), logotype placed, fonts loaded, Escape pause, branded loader. Run \`npx genex wait\` on every generation you enqueued and wire what landed \u2014 never park landed assets.
|
|
269
|
-
13. Characters: Meshy/Mixamo/VRM rigs rest facing +Z. Set yaw explicitly when placing a rig; never mirror a SkinnedMesh with negative scale. In any two-character scene, verify in a capture that they face each other, not the camera.
|
|
270
|
-
14. Verify by looking: one smoke check per milestone, after that milestone's preview push, in local test mode (\`?genex_local_test=1\`) with a real gameplay screenshot. A claim without a capture is not verification.
|
|
268
|
+
12. Wire-in floors before any publish and before ending a session: sprite HUD wired (or the recorded CSS lane finished), menu video playing (or its recorded still fallback), logotype placed, fonts loaded, Escape pause, branded loader, the player wearing the game's own generated character (or DESIGN.md recording why it isn't). Run \`npx genex wait\` on every generation you enqueued and wire what landed \u2014 never park landed assets.
|
|
269
|
+
13. The player's body is the game's own generated character (\`npx genex character "<look>"\` \u2192 \`npx genex controller character --character <id>\`), enqueued with your first art actions, not after them. It applies wherever a human body appears on screen \u2014 first-person included, the moment remotes, a look-down body, a shadow, or a menu portrait shows one. The profile VRM avatar is the FALLBACK: a temporary body while the character renders (say in one plain line that it's temporary), or the stand-in when generation genuinely could not happen \u2014 out of credits, failed, unverified; record which in DESIGN.md as \`Player character: VRM \u2014 <reason>\`. Games whose player is not a person (car, ship, RTS cursor, board) generate that object with \`npx genex model\` instead. Characters: Meshy/Mixamo/VRM rigs rest facing +Z. Set yaw explicitly when placing a rig; never mirror a SkinnedMesh with negative scale. In any two-character scene, verify in a capture that they face each other, not the camera.
|
|
270
|
+
14. Verify by looking: one smoke check per milestone, after that milestone's preview push, in local test mode (\`?genex_local_test=1\`) with a real gameplay screenshot. A claim without a capture is not verification. Local-test evidence proves visuals and controls ONLY \u2014 label it that way when you show the player, and never work around the draft sign-in gate any other way.
|
|
271
271
|
15. Treat every \`genex\` warning line \u2014 preflight, \`ui audit\`, \`wait\` nudges \u2014 as work, not noise.
|
|
272
272
|
16. Every finished Build-plan milestone ends with \`npx genex preview\` and the player's page link (\`<dashboard>/draft/<slug>\` with \`<dashboard>\` from \`.genex/project.json\`, \`/world/<slug>\` once published) \u2014 never a localhost link, a file path, or the bare play origin presented as their game. After every round of player feedback, end with a preview push.
|
|
273
273
|
17. Parallel work: this line is your standing authorization and request to use sub-agents / parallel agent work whenever your platform provides them. While drafting the Build plan, decide per module what runs in parallel and what stays serial for THIS game \u2014 dependencies decide, there is no fixed list \u2014 and record each call in the Modules table with a one-line reason. Independent modules default to parallel; building everything serially needs a stated reason. You keep integration, previews, and the player conversation; each sub-agent owns only its module's files.
|
|
274
274
|
18. Talk to the player in plain game language \u2014 what changed in the game and what to try; never code, file names, build output, or tool internals unless they ask. Short status lines while you work; long silent stretches are a failure.
|
|
275
|
+
19. Never add debug-only code to the game to check your own work \u2014 no hidden test modes, no special URL parameters, no forced-visible flags, no auth mocks, no pixel-sampling hooks. \`?genex_local_test=1\` is the platform's own supported mode and is fine; your own bypass is not. (The multiplayer skill's small build identifier, token-free status line, and connected-quorum watchdog are production supportability, not a bypass \u2014 keep those.)
|
|
276
|
+
20. Input directions match their labels: A/\u2190 moves or turns the player screen-LEFT, D/\u2192 screen-RIGHT, mouse-up looks up, and drag-pan axes share ONE convention. The cursor is either the gameplay tool (RTS, card, builder) or locked away during play \u2014 keyboard-only games included. Check it in every milestone's smoke pass.
|
|
277
|
+
21. v0 is a milestone, not the destination. When the ask was bigger than one loop, every milestone after v0 grows back toward the FULL ask with DESIGN.md's content lines as the checklist \u2014 a slice that previewed well never quietly becomes the game. Cosmetics never jump the queue past promised content.
|
|
275
278
|
${CONTRACT_END}
|
|
276
279
|
`;
|
|
277
280
|
var CLAUDE_IMPORT_LINE = "@AGENTS.md";
|
|
@@ -2113,6 +2116,7 @@ var UI_USAGE = `${c.bold("Options for `ui` (all tools read --in <png path or htt
|
|
|
2113
2116
|
white halo over dark scenes). [--expect N: hard-error
|
|
2114
2117
|
unless exactly N components] [--min-pixels 2000]
|
|
2115
2118
|
[--padding 8] [--chroma 240] [--dilate 0] [--no-defringe]
|
|
2119
|
+
[--allow-speckle]
|
|
2116
2120
|
ui masks --out-dir <dir> --pairs "name:cX,cY,w,h:aX,aY,w,h[:n];\u2026"
|
|
2117
2121
|
OR --auto (detect clean/annotated cell pairs itself) \u2014
|
|
2118
2122
|
clean frame + green-annotated duplicate \u2192 frame + fill mask
|
|
@@ -2128,6 +2132,9 @@ var UI_USAGE = `${c.bold("Options for `ui` (all tools read --in <png path or htt
|
|
|
2128
2132
|
[--min-frac 0.004] [--out <json>] [--crop <png>]
|
|
2129
2133
|
ui trim [--out <png>] \u2014 crop to the alpha bbox (default: overwrite
|
|
2130
2134
|
--in), print real dims + refresh the .bbox.json sidecar.
|
|
2135
|
+
Hard-refuses a DAMAGED cutout (isolated colour speckle on
|
|
2136
|
+
the soft rim \u2014 what a background-removal pass does to an
|
|
2137
|
+
image that was already transparent). [--allow-speckle]
|
|
2131
2138
|
ui seams measure a tiling texture's wrap seam (opposite edges vs
|
|
2132
2139
|
interior detail) \u2192 ratio + verdict as JSON; nonzero exit on
|
|
2133
2140
|
a visible seam. [--seam-tolerance 3]
|
|
@@ -2139,7 +2146,9 @@ var UI_USAGE = `${c.bold("Options for `ui` (all tools read --in <png path or htt
|
|
|
2139
2146
|
ui audit mechanical HUD-wiring scan (no --in): mask\u2194frame pairing
|
|
2140
2147
|
mismatches (recorded crops disagree \u2014 PROVABLE, exit 1),
|
|
2141
2148
|
white-painted meter troughs (baked sheet-white inside a
|
|
2142
|
-
fill channel \u2014 PROVABLE, exit 1),
|
|
2149
|
+
fill channel \u2014 PROVABLE, exit 1), damaged cutouts (colour
|
|
2150
|
+
speckle on a wired sprite's rim \u2014 PROVABLE, exit 1),
|
|
2151
|
+
loosened-check stamps,
|
|
2143
2152
|
extracted-but-never-wired sprites, generated-but-
|
|
2144
2153
|
unreferenced masks, naive JS %-width fills, phones-
|
|
2145
2154
|
preload-video heuristic. Everything but the two provable
|
|
@@ -2422,6 +2431,15 @@ async function uiExtract(opts, log) {
|
|
|
2422
2431
|
if (inputMode === "chroma" && opts.defringe !== false) {
|
|
2423
2432
|
defringed = defringeAgainstMatte(out) > 0;
|
|
2424
2433
|
}
|
|
2434
|
+
const speckle = rimSpeckleRatio(out);
|
|
2435
|
+
if (speckle && speckle.ratio > SPECKLE_MAX_RATIO && !opts.allowSpeckle) {
|
|
2436
|
+
fail(speckleRefusal(name, speckle.ratio), {
|
|
2437
|
+
input,
|
|
2438
|
+
sprite: name,
|
|
2439
|
+
speckleRatio: Number(speckle.ratio.toFixed(4)),
|
|
2440
|
+
rimPixels: speckle.sampled
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
2425
2443
|
const outPath = path11.join(outDir, `${name}.png`);
|
|
2426
2444
|
await writePng(outPath, out);
|
|
2427
2445
|
const sidecar = {
|
|
@@ -3180,13 +3198,30 @@ async function uiTrim(opts, log) {
|
|
|
3180
3198
|
h: bbox.maxY - bbox.minY + 1
|
|
3181
3199
|
};
|
|
3182
3200
|
const trimmed = cropPng(png, cropBox);
|
|
3201
|
+
const speckle = rimSpeckleRatio(trimmed);
|
|
3202
|
+
if (speckle && speckle.ratio > SPECKLE_MAX_RATIO && !opts.allowSpeckle) {
|
|
3203
|
+
fail(speckleRefusal("this image", speckle.ratio), {
|
|
3204
|
+
input,
|
|
3205
|
+
speckleRatio: Number(speckle.ratio.toFixed(4)),
|
|
3206
|
+
rimPixels: speckle.sampled
|
|
3207
|
+
});
|
|
3208
|
+
}
|
|
3183
3209
|
await writePng(outPath, trimmed);
|
|
3184
3210
|
const sidecar = computeBBoxes(trimmed);
|
|
3211
|
+
const speckleAllowed = !!(speckle && speckle.ratio > SPECKLE_MAX_RATIO);
|
|
3185
3212
|
const sidecarPath = outPath.replace(/\.png$/i, "") + ".bbox.json";
|
|
3186
|
-
await fs11.writeFile(
|
|
3213
|
+
await fs11.writeFile(
|
|
3214
|
+
sidecarPath,
|
|
3215
|
+
JSON.stringify(speckleAllowed ? { ...sidecar, speckleAllowed: true } : sidecar, null, 2)
|
|
3216
|
+
);
|
|
3187
3217
|
log.success(
|
|
3188
3218
|
`Trimmed ${png.width}x${png.height} \u2192 ${trimmed.width}x${trimmed.height} (${outPath}).`
|
|
3189
3219
|
);
|
|
3220
|
+
if (speckleAllowed) {
|
|
3221
|
+
log.warn(
|
|
3222
|
+
"Shipped with --allow-speckle: this cutout is damaged and the override is stamped in its sidecar. Eyeball it over a bright background before it reaches the player."
|
|
3223
|
+
);
|
|
3224
|
+
}
|
|
3190
3225
|
process.stdout.write(
|
|
3191
3226
|
`${JSON.stringify({
|
|
3192
3227
|
out: outPath,
|
|
@@ -3195,7 +3230,9 @@ async function uiTrim(opts, log) {
|
|
|
3195
3230
|
aspectRatio: sidecar.aspectRatio,
|
|
3196
3231
|
outerBBox: sidecar.outerBBox,
|
|
3197
3232
|
innerBBox: sidecar.innerBBox,
|
|
3198
|
-
sidecar: sidecarPath
|
|
3233
|
+
sidecar: sidecarPath,
|
|
3234
|
+
...speckle ? { speckleRatio: Number(speckle.ratio.toFixed(4)) } : {},
|
|
3235
|
+
...speckleAllowed ? { speckleAllowed: true } : {}
|
|
3199
3236
|
})}
|
|
3200
3237
|
`
|
|
3201
3238
|
);
|
|
@@ -3292,6 +3329,51 @@ async function uiPlate(opts, log) {
|
|
|
3292
3329
|
`
|
|
3293
3330
|
);
|
|
3294
3331
|
}
|
|
3332
|
+
var SPECKLE_RIM_ALPHA_MIN = 16;
|
|
3333
|
+
var SPECKLE_RIM_ALPHA_MAX = 220;
|
|
3334
|
+
var SPECKLE_CHROMA_EXCESS = 60;
|
|
3335
|
+
var SPECKLE_MIN_SAMPLE = 200;
|
|
3336
|
+
var SPECKLE_MAX_RATIO = 0.02;
|
|
3337
|
+
function rimSpeckleRatio(png) {
|
|
3338
|
+
const { width: W, height: H, data } = png;
|
|
3339
|
+
if (W < 3 || H < 3) return null;
|
|
3340
|
+
const chroma = new Uint8Array(W * H);
|
|
3341
|
+
for (let i = 0; i < W * H; i++) {
|
|
3342
|
+
const o = i * 4;
|
|
3343
|
+
const r = data[o];
|
|
3344
|
+
const g = data[o + 1];
|
|
3345
|
+
const b = data[o + 2];
|
|
3346
|
+
chroma[i] = Math.max(r, g, b) - Math.min(r, g, b);
|
|
3347
|
+
}
|
|
3348
|
+
const neighbourhood = [];
|
|
3349
|
+
let sampled = 0;
|
|
3350
|
+
let speckled = 0;
|
|
3351
|
+
for (let y = 1; y < H - 1; y++) {
|
|
3352
|
+
for (let x = 1; x < W - 1; x++) {
|
|
3353
|
+
const i = y * W + x;
|
|
3354
|
+
const a = data[i * 4 + 3];
|
|
3355
|
+
if (a <= SPECKLE_RIM_ALPHA_MIN || a >= SPECKLE_RIM_ALPHA_MAX) continue;
|
|
3356
|
+
sampled++;
|
|
3357
|
+
neighbourhood.length = 0;
|
|
3358
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
3359
|
+
for (let dx = -1; dx <= 1; dx++) neighbourhood.push(chroma[(y + dy) * W + (x + dx)]);
|
|
3360
|
+
}
|
|
3361
|
+
neighbourhood.sort((p, q) => p - q);
|
|
3362
|
+
if (chroma[i] - neighbourhood[4] > SPECKLE_CHROMA_EXCESS) speckled++;
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
if (sampled < SPECKLE_MIN_SAMPLE) return null;
|
|
3366
|
+
return { ratio: speckled / sampled, sampled };
|
|
3367
|
+
}
|
|
3368
|
+
function speckleRefusal(label, ratio) {
|
|
3369
|
+
return [
|
|
3370
|
+
`Damaged cutout \u2014 ${(ratio * 100).toFixed(1)}% of ${label}'s soft edge is isolated colour speckle (want <= ${(SPECKLE_MAX_RATIO * 100).toFixed(0)}%).`,
|
|
3371
|
+
"A background-removal pass over an image that was ALREADY transparent re-keys every edge and sprays colour noise along it. Trimming or defringing cannot fix this.",
|
|
3372
|
+
" -> From `genex image --clean`? Ship the `--transparent` original instead.",
|
|
3373
|
+
" -> Source really had a background? Re-clean with `--bg-mode matte` (soft edges).",
|
|
3374
|
+
" Override (stamped in the output): --allow-speckle"
|
|
3375
|
+
].join("\n");
|
|
3376
|
+
}
|
|
3295
3377
|
var DEFRINGE_BAND_PX = 2;
|
|
3296
3378
|
function defringeAgainstMatte(png, matte = { r: 255, g: 255, b: 255 }) {
|
|
3297
3379
|
const { width: W, height: H, data } = png;
|
|
@@ -3374,7 +3456,8 @@ function defringeAgainstMatte(png, matte = { r: 255, g: 255, b: 255 }) {
|
|
|
3374
3456
|
}
|
|
3375
3457
|
var HARD_AUDIT_KINDS = /* @__PURE__ */ new Set([
|
|
3376
3458
|
"mask-frame-mismatch",
|
|
3377
|
-
"white-trough"
|
|
3459
|
+
"white-trough",
|
|
3460
|
+
"cutout-speckle"
|
|
3378
3461
|
]);
|
|
3379
3462
|
var TROUGH_OPAQUE_MIN_ALPHA = 200;
|
|
3380
3463
|
var TROUGH_WHITE_MIN = 235;
|
|
@@ -3453,7 +3536,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
3453
3536
|
} catch {
|
|
3454
3537
|
}
|
|
3455
3538
|
}
|
|
3456
|
-
const referenced = (
|
|
3539
|
+
const referenced = (basename2) => sources.some((s) => s.text.includes(basename2));
|
|
3457
3540
|
const findings = [...viewportFindings];
|
|
3458
3541
|
const assetFiles = await walkFiles(absAssets);
|
|
3459
3542
|
const metaByName = /* @__PURE__ */ new Map();
|
|
@@ -3535,6 +3618,22 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
3535
3618
|
}
|
|
3536
3619
|
}
|
|
3537
3620
|
}
|
|
3621
|
+
for (const [base, p] of pngByBase) {
|
|
3622
|
+
if (/-mask\.png$/.test(base)) continue;
|
|
3623
|
+
if (!referenced(base)) continue;
|
|
3624
|
+
let png;
|
|
3625
|
+
try {
|
|
3626
|
+
png = PNG2.sync.read(await fs11.readFile(p));
|
|
3627
|
+
} catch {
|
|
3628
|
+
continue;
|
|
3629
|
+
}
|
|
3630
|
+
const speckle = rimSpeckleRatio(png);
|
|
3631
|
+
if (!speckle || speckle.ratio <= SPECKLE_MAX_RATIO) continue;
|
|
3632
|
+
findings.push({
|
|
3633
|
+
kind: "cutout-speckle",
|
|
3634
|
+
message: `${base}: ${(speckle.ratio * 100).toFixed(1)}% of the soft edge is isolated colour speckle (want <= ${(SPECKLE_MAX_RATIO * 100).toFixed(0)}%) \u2014 a background-removal pass re-keyed edges that were already transparent and sprayed colour noise along them. Nothing downstream repairs it: ship the \`--transparent\` original, or re-clean the true source with \`--bg-mode matte\`. Do not ship this sprite.`
|
|
3635
|
+
});
|
|
3636
|
+
}
|
|
3538
3637
|
const loosenedNames = [...metaByName.entries()].filter(([, meta]) => meta.loosened).map(([name]) => name);
|
|
3539
3638
|
if (loosenedNames.length > 0) {
|
|
3540
3639
|
findings.push({
|
|
@@ -3866,13 +3965,109 @@ async function inlineLocalImage(filePath, flag) {
|
|
|
3866
3965
|
const mime = IMAGE_MIME_BY_EXT[path13.extname(filePath).toLowerCase()] ?? "image/png";
|
|
3867
3966
|
return { ok: true, dataUri: `data:${mime};base64,${bytes.toString("base64")}` };
|
|
3868
3967
|
}
|
|
3869
|
-
var SKYBOX_ENVIRONMENT_SUFFIX = ".
|
|
3968
|
+
var SKYBOX_ENVIRONMENT_SUFFIX = ". The image contains ONLY sky: cloud, atmosphere, light, weather and distant haze at the horizon. Every structure, object, plant and ground surface is outside the frame.";
|
|
3870
3969
|
var SKYBOX_PROMPT_MAX = 2e3;
|
|
3970
|
+
var SKYBOX_STRUCTURE_NOUNS = [
|
|
3971
|
+
"building",
|
|
3972
|
+
"buildings",
|
|
3973
|
+
"house",
|
|
3974
|
+
"houses",
|
|
3975
|
+
"hut",
|
|
3976
|
+
"huts",
|
|
3977
|
+
"cabin",
|
|
3978
|
+
"cabins",
|
|
3979
|
+
"tower",
|
|
3980
|
+
"towers",
|
|
3981
|
+
"castle",
|
|
3982
|
+
"castles",
|
|
3983
|
+
"cathedral",
|
|
3984
|
+
"cathedrals",
|
|
3985
|
+
"church",
|
|
3986
|
+
"churches",
|
|
3987
|
+
"temple",
|
|
3988
|
+
"temples",
|
|
3989
|
+
"shrine",
|
|
3990
|
+
"shrines",
|
|
3991
|
+
"monastery",
|
|
3992
|
+
"palace",
|
|
3993
|
+
"fortress",
|
|
3994
|
+
"citadel",
|
|
3995
|
+
"ruin",
|
|
3996
|
+
"ruins",
|
|
3997
|
+
"city",
|
|
3998
|
+
"cityscape",
|
|
3999
|
+
"skyline",
|
|
4000
|
+
"town",
|
|
4001
|
+
"village",
|
|
4002
|
+
"settlement",
|
|
4003
|
+
"street",
|
|
4004
|
+
"streets",
|
|
4005
|
+
"road",
|
|
4006
|
+
"roads",
|
|
4007
|
+
"courtyard",
|
|
4008
|
+
"plaza",
|
|
4009
|
+
"bridge",
|
|
4010
|
+
"bridges",
|
|
4011
|
+
"wall",
|
|
4012
|
+
"walls",
|
|
4013
|
+
"gate",
|
|
4014
|
+
"gates",
|
|
4015
|
+
"fence",
|
|
4016
|
+
"fences",
|
|
4017
|
+
"pillar",
|
|
4018
|
+
"pillars",
|
|
4019
|
+
"column",
|
|
4020
|
+
"columns",
|
|
4021
|
+
"statue",
|
|
4022
|
+
"statues",
|
|
4023
|
+
"monument",
|
|
4024
|
+
"obelisk",
|
|
4025
|
+
"spire",
|
|
4026
|
+
"spires",
|
|
4027
|
+
"rooftop",
|
|
4028
|
+
"rooftops",
|
|
4029
|
+
"tree",
|
|
4030
|
+
"trees",
|
|
4031
|
+
"forest",
|
|
4032
|
+
"jungle",
|
|
4033
|
+
"ship",
|
|
4034
|
+
"ships",
|
|
4035
|
+
"boat",
|
|
4036
|
+
"boats",
|
|
4037
|
+
"car",
|
|
4038
|
+
"cars",
|
|
4039
|
+
"vehicle",
|
|
4040
|
+
"vehicles",
|
|
4041
|
+
"person",
|
|
4042
|
+
"people",
|
|
4043
|
+
"figure",
|
|
4044
|
+
"figures",
|
|
4045
|
+
"character",
|
|
4046
|
+
"characters",
|
|
4047
|
+
"creature",
|
|
4048
|
+
"creatures",
|
|
4049
|
+
"dragon",
|
|
4050
|
+
"banner",
|
|
4051
|
+
"banners",
|
|
4052
|
+
"flag",
|
|
4053
|
+
"flags",
|
|
4054
|
+
"torch",
|
|
4055
|
+
"torches",
|
|
4056
|
+
"lamp",
|
|
4057
|
+
"lamps"
|
|
4058
|
+
];
|
|
4059
|
+
var SKYBOX_NOUN_RE = new RegExp(`\\b(${SKYBOX_STRUCTURE_NOUNS.join("|")})\\b`, "gi");
|
|
4060
|
+
function skyboxStructureNouns(prompt) {
|
|
4061
|
+
const found = /* @__PURE__ */ new Set();
|
|
4062
|
+
for (const m of prompt.matchAll(SKYBOX_NOUN_RE)) found.add(m[1].toLowerCase());
|
|
4063
|
+
return [...found];
|
|
4064
|
+
}
|
|
3871
4065
|
function applySkyboxGuard(prompt, raw) {
|
|
3872
|
-
if (raw) return { prompt, guarded: false };
|
|
4066
|
+
if (raw) return { prompt, guarded: false, nouns: [] };
|
|
4067
|
+
const nouns = skyboxStructureNouns(prompt);
|
|
3873
4068
|
const guarded = `${prompt}${SKYBOX_ENVIRONMENT_SUFFIX}`;
|
|
3874
|
-
if (guarded.length > SKYBOX_PROMPT_MAX) return { prompt, guarded: false };
|
|
3875
|
-
return { prompt: guarded, guarded: true };
|
|
4069
|
+
if (guarded.length > SKYBOX_PROMPT_MAX) return { prompt, guarded: false, nouns };
|
|
4070
|
+
return { prompt: guarded, guarded: true, nouns };
|
|
3876
4071
|
}
|
|
3877
4072
|
function buildGenOptions(kind, opts) {
|
|
3878
4073
|
const options = { ...opts.generationOptions };
|
|
@@ -3927,6 +4122,23 @@ async function runGenerate(kind, opts) {
|
|
|
3927
4122
|
return;
|
|
3928
4123
|
}
|
|
3929
4124
|
const skyboxGuard = kind === "skybox" ? applySkyboxGuard(typedPrompt, opts.raw) : void 0;
|
|
4125
|
+
if (skyboxGuard && skyboxGuard.nouns.length > 0) {
|
|
4126
|
+
log.error(
|
|
4127
|
+
`A skybox is sky only \u2014 this prompt names ${skyboxGuard.nouns.join(", ")}.`
|
|
4128
|
+
);
|
|
4129
|
+
log.plain(
|
|
4130
|
+
[
|
|
4131
|
+
" Structures painted into a sky render at infinite distance: they never get closer",
|
|
4132
|
+
" as the player moves, and they read as broken the moment the camera strafes.",
|
|
4133
|
+
` ${c.cyan("\u2192")} Build them as scene geometry (${c.cyan("npx genex model")}); describe only light, colour,`,
|
|
4134
|
+
" cloud, weather and time of day here.",
|
|
4135
|
+
` ${c.cyan("\u2192")} Genuinely want it painted into the sky (a space-station panorama for a scene`,
|
|
4136
|
+
` with no world geometry)? Re-run with ${c.cyan("--raw")}.`
|
|
4137
|
+
].join("\n")
|
|
4138
|
+
);
|
|
4139
|
+
process.exitCode = 1;
|
|
4140
|
+
return;
|
|
4141
|
+
}
|
|
3930
4142
|
let prompt = skyboxGuard?.prompt ?? typedPrompt;
|
|
3931
4143
|
if (kind === "image" && opts.glass) {
|
|
3932
4144
|
const conflict = opts.transparent ? "--transparent" : opts.removeBackground ? "--remove-bg" : opts.editUrl ? "--edit" : opts.cleanUrl ? "--clean" : opts.upscaleUrl ? "--upscale" : opts.noWait ? "--no-wait" : void 0;
|
|
@@ -4264,7 +4476,10 @@ var KIND_WAIT_TIMEOUT_MS = {
|
|
|
4264
4476
|
character_concept: 15 * 60 * 1e3,
|
|
4265
4477
|
character_preview: 30 * 60 * 1e3,
|
|
4266
4478
|
character: 30 * 60 * 1e3,
|
|
4267
|
-
character_animation: 20 * 60 * 1e3
|
|
4479
|
+
character_animation: 20 * 60 * 1e3,
|
|
4480
|
+
// Per clip this lane is fast (locomotion ~2s, video ~51s, described ~130s) —
|
|
4481
|
+
// the BATCH is what takes time. A 20-clip standard set is the sizing case.
|
|
4482
|
+
character_motion: 30 * 60 * 1e3
|
|
4268
4483
|
};
|
|
4269
4484
|
var waitTimeoutFor = (kind) => KIND_WAIT_TIMEOUT_MS[kind] ?? WAIT_TIMEOUT_MS;
|
|
4270
4485
|
var TERMINAL = /* @__PURE__ */ new Set(["completed", "failed"]);
|
|
@@ -4380,7 +4595,8 @@ function printHint(kind, view, files, log) {
|
|
|
4380
4595
|
character_concept: "Show all three concept candidates to the user and wait for an explicit selection.",
|
|
4381
4596
|
character_preview: "Show all four preview views to the user and wait for explicit approval before finalizing.",
|
|
4382
4597
|
character: `Install the controller and current manifest with genex controller character --character ${view.id}.`,
|
|
4383
|
-
character_animation: "The action is now part of the character's current manifest; refresh the local controller manifest before testing."
|
|
4598
|
+
character_animation: "The action is now part of the character's current manifest; refresh the local controller manifest before testing.",
|
|
4599
|
+
character_motion: `The clips are part of the character's current manifest \u2014 install them with genex controller character --character ${view.id}, then play the game and watch the motion on the real character before calling it done.`
|
|
4384
4600
|
};
|
|
4385
4601
|
log.dim(` ${hint[kind]}`);
|
|
4386
4602
|
log.dim(" Reference the URL directly in your code \u2014 don't download it into the repo.");
|
|
@@ -4660,6 +4876,14 @@ async function printPipelineStatePreflight(log, cwd = process.cwd()) {
|
|
|
4660
4876
|
);
|
|
4661
4877
|
}
|
|
4662
4878
|
}
|
|
4879
|
+
if (!/player character:/i.test(design)) {
|
|
4880
|
+
const hasCharacter = await fs14.access(path14.join(cwd, "public", "assets", "meshy-character.json")).then(() => true, () => false);
|
|
4881
|
+
if (!hasCharacter && await loadsPlayerBody(cwd)) {
|
|
4882
|
+
warnings.push(
|
|
4883
|
+
`Player character is the stock avatar \u2014 no generated character is wired. The game's own generated character is the player's body wherever a human body appears on screen, first-person included (genex-ai-character). Generate it, or record "Player character: VRM \u2014 <reason>" in DESIGN.md (no human body in this game / out of credits / player declined).`
|
|
4884
|
+
);
|
|
4885
|
+
}
|
|
4886
|
+
}
|
|
4663
4887
|
for (const w of warnings) log.warn(w);
|
|
4664
4888
|
} catch {
|
|
4665
4889
|
}
|
|
@@ -4687,6 +4911,21 @@ async function hasHudLikeDom(cwd) {
|
|
|
4687
4911
|
}
|
|
4688
4912
|
return false;
|
|
4689
4913
|
}
|
|
4914
|
+
async function loadsPlayerBody(cwd) {
|
|
4915
|
+
const BODY_LOADERS = /\bloadPlayerCharacter\s*\(|\bloadVrm(?:Clone)?\s*\(/;
|
|
4916
|
+
try {
|
|
4917
|
+
const entries = await fs14.readdir(path14.join(cwd, "src"), { recursive: true });
|
|
4918
|
+
for (const rel of entries) {
|
|
4919
|
+
if (rel.includes("node_modules")) continue;
|
|
4920
|
+
if (rel.split(path14.sep)[0] === "controllers" || rel.startsWith("controllers/")) continue;
|
|
4921
|
+
if (!/\.(ts|tsx|js|jsx)$/.test(rel)) continue;
|
|
4922
|
+
const text = await fs14.readFile(path14.join(cwd, "src", rel), "utf8").catch(() => "");
|
|
4923
|
+
if (BODY_LOADERS.test(text)) return true;
|
|
4924
|
+
}
|
|
4925
|
+
} catch {
|
|
4926
|
+
}
|
|
4927
|
+
return false;
|
|
4928
|
+
}
|
|
4690
4929
|
async function hasExtractionSidecars(cwd) {
|
|
4691
4930
|
try {
|
|
4692
4931
|
const entries = await fs14.readdir(path14.join(cwd, "public"), { recursive: true });
|
|
@@ -15372,9 +15611,14 @@ var CONTROLLER_FILE_SETS = {
|
|
|
15372
15611
|
"character/animation-packs.ts",
|
|
15373
15612
|
"character/motion-actions.ts",
|
|
15374
15613
|
"character/meshy/meshy-loader.ts",
|
|
15614
|
+
// The player's body, resolved once: this game's own generated character
|
|
15615
|
+
// when it has one, the visiting player's avatar when it doesn't. ONE
|
|
15616
|
+
// shape for both lanes, so a character that lands at hour 2 is a file
|
|
15617
|
+
// drop and not a rewrite of the boot path written at hour 0.
|
|
15618
|
+
"character/player-character.ts",
|
|
15375
15619
|
"character/presets.ts",
|
|
15376
15620
|
// VRM avatar support (three-vrm): load + retarget the UAL clips + auto-fit
|
|
15377
|
-
// the capsule + optional foot IK
|
|
15621
|
+
// the capsule + optional foot IK — the FALLBACK body's lane.
|
|
15378
15622
|
"character/vrm/vrm-loader.ts",
|
|
15379
15623
|
"character/vrm/vrm-retarget.ts",
|
|
15380
15624
|
"character/vrm/capsule-fit.ts",
|
|
@@ -15383,22 +15627,27 @@ var CONTROLLER_FILE_SETS = {
|
|
|
15383
15627
|
NOTICE
|
|
15384
15628
|
],
|
|
15385
15629
|
// The FALLBACK VRM is written to public/assets/avatar.vrm at install time by
|
|
15386
|
-
//
|
|
15387
|
-
//
|
|
15388
|
-
//
|
|
15389
|
-
//
|
|
15390
|
-
//
|
|
15391
|
-
//
|
|
15630
|
+
// installFallbackAvatar (the bundled CC0 default) — so it is NOT a static
|
|
15631
|
+
// manifest asset; at runtime the game loads the playing user's own avatar
|
|
15632
|
+
// (user.avatarUrl from the embed identity, AG-804) and this file covers
|
|
15633
|
+
// local dev + load failures. It is only ever the FALLBACK body: the player
|
|
15634
|
+
// is this game's own generated character whenever it has one.
|
|
15635
|
+
// animation-library.glb (the 12-clip core) still is a manifest asset; extra
|
|
15636
|
+
// clips arrive via `genex controller anims` into public/assets/anims/.
|
|
15392
15637
|
assets: ["assets/animation-library.glb"],
|
|
15393
15638
|
skill: "genex-threejs-character-controller",
|
|
15639
|
+
// ONE sketch, with or without --character. The lane is a runtime decision
|
|
15640
|
+
// inside loadPlayerCharacter, never a code edit the agent has to remember
|
|
15641
|
+
// to make once the generated character lands.
|
|
15394
15642
|
sketch: [
|
|
15395
15643
|
`const physics = await PhysicsWorld.create();`,
|
|
15396
|
-
`const { user } = await waitForPlayer(); // @genex-ai/embed-sdk
|
|
15397
|
-
`const
|
|
15398
|
-
`const
|
|
15399
|
-
`const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...
|
|
15400
|
-
`character.root.add(scene);
|
|
15401
|
-
`
|
|
15644
|
+
`const { user } = await waitForPlayer(); // @genex-ai/embed-sdk`,
|
|
15645
|
+
`const player = await loadPlayerCharacter({ avatarUrl: user.avatarUrl }); // this game's character, else the avatar fallback`,
|
|
15646
|
+
`const fit = capsuleFromModel(player.scene);`,
|
|
15647
|
+
`const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...fit, position: { x: 0, y: 2, z: 0 } });`,
|
|
15648
|
+
`character.root.add(player.scene); player.scene.position.y = fit.modelOffsetY;`,
|
|
15649
|
+
`const anims = new CharacterAnimations(player.scene, player.clips, { locomotionProfile: player.locomotionProfile });`,
|
|
15650
|
+
`addEventListener("pointerdown", () => anims.playOneShot("Punch_Jab")); // per frame: anims.update(character, dt); player.update(dt);`
|
|
15402
15651
|
]
|
|
15403
15652
|
},
|
|
15404
15653
|
car: {
|
|
@@ -15557,8 +15806,7 @@ async function runController(opts) {
|
|
|
15557
15806
|
return;
|
|
15558
15807
|
}
|
|
15559
15808
|
} else {
|
|
15560
|
-
|
|
15561
|
-
await installOwnerAvatar({ root, srcDir, apiUrl: getApiUrl(opts.apiUrl), token, log });
|
|
15809
|
+
await installFallbackAvatar({ root, srcDir, log });
|
|
15562
15810
|
}
|
|
15563
15811
|
log.plain("");
|
|
15564
15812
|
}
|
|
@@ -15577,16 +15825,18 @@ async function runController(opts) {
|
|
|
15577
15825
|
log.plain(
|
|
15578
15826
|
kind === "touch" ? ` ${stepOffset + 2}. Wiring sketch (create behind a touch check; read per frame):` : ` ${stepOffset + 2}. Wiring sketch (controllers update BEFORE the physics step):`
|
|
15579
15827
|
);
|
|
15580
|
-
const
|
|
15581
|
-
`const physics = await PhysicsWorld.create();`,
|
|
15582
|
-
`const native = await loadMeshyCharacter("./assets/meshy-character.json");`,
|
|
15583
|
-
`const fit = capsuleFromModel(native.scene); const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...fit, position: { x: 0, y: 2, z: 0 } });`,
|
|
15584
|
-
`character.root.add(native.scene); const anims = new CharacterAnimations(native.scene, native.clips, { locomotionProfile: native.locomotionProfile });`,
|
|
15585
|
-
`physics.onBeforeStep(() => character.update(dt, input)); // Rapier owns movement; then anims.update(character, dt)`
|
|
15586
|
-
] : set.sketch;
|
|
15587
|
-
for (const line of sketch) {
|
|
15828
|
+
for (const line of set.sketch) {
|
|
15588
15829
|
log.dim(` ${line}`);
|
|
15589
15830
|
}
|
|
15831
|
+
if (kind === "character" && !await exists3(path17.join(root, ASSETS_DEST, "meshy-character.json"))) {
|
|
15832
|
+
log.plain("");
|
|
15833
|
+
log.plain(
|
|
15834
|
+
` ${stepOffset + 3}. This game has no generated character yet \u2014 ${c.cyan(
|
|
15835
|
+
'npx genex character "<look>"'
|
|
15836
|
+
)} makes the player's real body;`
|
|
15837
|
+
);
|
|
15838
|
+
log.plain(" the avatar above is only the fallback until it lands. No code change when it does.");
|
|
15839
|
+
}
|
|
15590
15840
|
}
|
|
15591
15841
|
async function installMeshyCharacterManifest(args) {
|
|
15592
15842
|
const response = await apiFetch(
|
|
@@ -15618,6 +15868,7 @@ async function installMeshyCharacterManifest(args) {
|
|
|
15618
15868
|
`
|
|
15619
15869
|
);
|
|
15620
15870
|
args.log.dim(` public/assets/meshy-character.json (${args.characterId}, current revision)`);
|
|
15871
|
+
args.log.success("This game's generated character is now the player's body \u2014 picked up on the next reload, no code change needed.");
|
|
15621
15872
|
const pack2 = manifest.controllerPack;
|
|
15622
15873
|
if (typeof pack2?.key === "string" && typeof pack2.version === "number") {
|
|
15623
15874
|
args.log.success(`Meshy controller pack ${pack2.key} v${pack2.version}`);
|
|
@@ -15747,36 +15998,12 @@ function assertCompleteMeshyControllerPack(manifest) {
|
|
|
15747
15998
|
incompletePack(`${pack2.key} v${pack2.version} fingerprint does not match its action bindings.`);
|
|
15748
15999
|
}
|
|
15749
16000
|
}
|
|
15750
|
-
async function
|
|
15751
|
-
const { root, srcDir,
|
|
16001
|
+
async function installFallbackAvatar(args) {
|
|
16002
|
+
const { root, srcDir, log } = args;
|
|
15752
16003
|
const dest = path17.join(root, ASSETS_DEST, "avatar.vrm");
|
|
15753
16004
|
await fs17.mkdir(path17.dirname(dest), { recursive: true });
|
|
15754
|
-
if (token) {
|
|
15755
|
-
try {
|
|
15756
|
-
const res = await apiFetch(`${apiUrl}/api/avatars/me`, {
|
|
15757
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
15758
|
-
});
|
|
15759
|
-
if (res.ok) {
|
|
15760
|
-
const me = await res.json();
|
|
15761
|
-
if (me.vrmUrl) {
|
|
15762
|
-
const vrmRes = await fetch(me.vrmUrl);
|
|
15763
|
-
if (vrmRes.ok) {
|
|
15764
|
-
const buf = Buffer.from(await vrmRes.arrayBuffer());
|
|
15765
|
-
await fs17.writeFile(dest, buf);
|
|
15766
|
-
log.dim(` public/assets/avatar.vrm (your avatar \u2014 ${(buf.length / 1e6).toFixed(1)} MB)`);
|
|
15767
|
-
return;
|
|
15768
|
-
}
|
|
15769
|
-
}
|
|
15770
|
-
}
|
|
15771
|
-
log.dim(" couldn't fetch your avatar; using the bundled default.");
|
|
15772
|
-
} catch {
|
|
15773
|
-
log.dim(" avatar fetch failed (offline?); using the bundled default.");
|
|
15774
|
-
}
|
|
15775
|
-
}
|
|
15776
16005
|
await fs17.copyFile(path17.join(srcDir, "assets", "default-avatar.vrm"), dest);
|
|
15777
|
-
log.dim(
|
|
15778
|
-
token ? " public/assets/avatar.vrm (bundled default)" : " public/assets/avatar.vrm (bundled default \u2014 sign in and re-run for your own)"
|
|
15779
|
-
);
|
|
16006
|
+
log.dim(" public/assets/avatar.vrm (fallback avatar \u2014 bundled CC0 default)");
|
|
15780
16007
|
}
|
|
15781
16008
|
async function exists3(p) {
|
|
15782
16009
|
try {
|
|
@@ -16263,6 +16490,226 @@ async function runCharacterAnimate(opts) {
|
|
|
16263
16490
|
});
|
|
16264
16491
|
}
|
|
16265
16492
|
|
|
16493
|
+
// src/commands/character-motion.ts
|
|
16494
|
+
import { readFile } from "fs/promises";
|
|
16495
|
+
import { basename } from "path";
|
|
16496
|
+
var VIDEO_SECONDS = [3, 4, 5, 6];
|
|
16497
|
+
var MAX_VIDEO_BYTES = 200 * 1024 * 1024;
|
|
16498
|
+
async function context2(opts) {
|
|
16499
|
+
const token = opts.token ?? await readUserToken(opts.envPath);
|
|
16500
|
+
if (!token) return null;
|
|
16501
|
+
const project = await readProject();
|
|
16502
|
+
return { token, apiUrl: getApiUrl(opts.apiUrl ?? project?.apiUrl) };
|
|
16503
|
+
}
|
|
16504
|
+
async function readVideo(path19, log) {
|
|
16505
|
+
let bytes;
|
|
16506
|
+
try {
|
|
16507
|
+
bytes = await readFile(path19);
|
|
16508
|
+
} catch {
|
|
16509
|
+
log.error(`Can't read ${path19}.`);
|
|
16510
|
+
return null;
|
|
16511
|
+
}
|
|
16512
|
+
if (bytes.byteLength > MAX_VIDEO_BYTES) {
|
|
16513
|
+
log.error(`${basename(path19)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
|
|
16514
|
+
return null;
|
|
16515
|
+
}
|
|
16516
|
+
return bytes;
|
|
16517
|
+
}
|
|
16518
|
+
async function uploadVideo(apiUrl, token, characterId, path19, bytes, log) {
|
|
16519
|
+
const contentType = /\.mov$/i.test(path19) ? "video/quicktime" : "video/mp4";
|
|
16520
|
+
const minted = await apiFetch(
|
|
16521
|
+
`${apiUrl}/api/characters/${encodeURIComponent(characterId)}/motions/video-url`,
|
|
16522
|
+
{
|
|
16523
|
+
method: "POST",
|
|
16524
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
16525
|
+
body: JSON.stringify({ bytes: bytes.byteLength, contentType })
|
|
16526
|
+
}
|
|
16527
|
+
);
|
|
16528
|
+
if (printedStructuredError(minted)) return null;
|
|
16529
|
+
if (!minted.ok) {
|
|
16530
|
+
log.error(`Couldn't start the video upload (HTTP ${minted.status}).`);
|
|
16531
|
+
return null;
|
|
16532
|
+
}
|
|
16533
|
+
const { uploadUrl, videoUrl } = await minted.json();
|
|
16534
|
+
log.dim(` uploading ${basename(path19)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
|
|
16535
|
+
const put = await fetch(uploadUrl, {
|
|
16536
|
+
method: "PUT",
|
|
16537
|
+
headers: { "Content-Type": contentType, "Content-Length": String(bytes.byteLength) },
|
|
16538
|
+
body: new Uint8Array(bytes)
|
|
16539
|
+
});
|
|
16540
|
+
if (!put.ok) {
|
|
16541
|
+
log.error(`Video upload failed (HTTP ${put.status}).`);
|
|
16542
|
+
return null;
|
|
16543
|
+
}
|
|
16544
|
+
return videoUrl;
|
|
16545
|
+
}
|
|
16546
|
+
function printPlan(plan, log, creature) {
|
|
16547
|
+
log.plain(c.bold(creature ? "Creature animation plan" : "Animation plan"));
|
|
16548
|
+
for (const item of plan.items) {
|
|
16549
|
+
const count = item.clips > 1 ? ` (${item.clips} clips)` : "";
|
|
16550
|
+
log.plain(` \xB7 ${c.bold(item.request)}${count} \u2014 ${item.explanation}`);
|
|
16551
|
+
if (item.supersededSlots.length > 0) {
|
|
16552
|
+
log.plain(
|
|
16553
|
+
` ${c.dim(`${item.supersededSlots.join(", ")} stay on the reviewed pack clips \u2014 those slots are pinned.`)}`
|
|
16554
|
+
);
|
|
16555
|
+
}
|
|
16556
|
+
}
|
|
16557
|
+
if (plan.secondsApplied === false) {
|
|
16558
|
+
log.plain(
|
|
16559
|
+
` ${c.dim("--duration sets the reference-video length; none of these verbs took the video route, so it did nothing.")}`
|
|
16560
|
+
);
|
|
16561
|
+
}
|
|
16562
|
+
log.plain("");
|
|
16563
|
+
if (plan.credits > 0) log.dim(` ${plan.totalClips} clips \xB7 ${plan.credits} credits`);
|
|
16564
|
+
else log.dim(` ${plan.totalClips} clips`);
|
|
16565
|
+
log.plain("");
|
|
16566
|
+
}
|
|
16567
|
+
async function runCharacterAnimateMotion(opts) {
|
|
16568
|
+
const log = createLogger({ quiet: opts.quiet || opts.json });
|
|
16569
|
+
const characterId = opts.characterId?.trim();
|
|
16570
|
+
if (!characterId) {
|
|
16571
|
+
log.error("Missing character id.");
|
|
16572
|
+
process.exitCode = 1;
|
|
16573
|
+
return;
|
|
16574
|
+
}
|
|
16575
|
+
const verbs = (opts.verbs ?? []).map((v) => v.trim()).filter(Boolean);
|
|
16576
|
+
if (verbs.length === 0 && !opts.locomotion) {
|
|
16577
|
+
log.error(
|
|
16578
|
+
`Say what the character should do. ${c.cyan(`genex character animate ${characterId} "overhead slam"`)}`
|
|
16579
|
+
);
|
|
16580
|
+
log.dim(` Or generate the movement set: ${c.cyan(`genex character animate ${characterId} --locomotion`)}`);
|
|
16581
|
+
process.exitCode = 1;
|
|
16582
|
+
return;
|
|
16583
|
+
}
|
|
16584
|
+
if (opts.video && verbs.length !== 1) {
|
|
16585
|
+
log.error("--video applies to exactly one verb \u2014 name the action the footage shows.");
|
|
16586
|
+
process.exitCode = 1;
|
|
16587
|
+
return;
|
|
16588
|
+
}
|
|
16589
|
+
if (opts.duration !== void 0 && !VIDEO_SECONDS.includes(opts.duration)) {
|
|
16590
|
+
log.error(`--duration must be one of ${VIDEO_SECONDS.join(", ")} seconds (got ${opts.duration}).`);
|
|
16591
|
+
process.exitCode = 1;
|
|
16592
|
+
return;
|
|
16593
|
+
}
|
|
16594
|
+
if (opts.duration !== void 0 && opts.video) {
|
|
16595
|
+
log.error("--duration sets the length of a GENERATED reference video \u2014 it cannot change footage you supplied.");
|
|
16596
|
+
process.exitCode = 1;
|
|
16597
|
+
return;
|
|
16598
|
+
}
|
|
16599
|
+
let videoBytes;
|
|
16600
|
+
if (opts.video) {
|
|
16601
|
+
const read = await readVideo(opts.video, log);
|
|
16602
|
+
if (!read) {
|
|
16603
|
+
process.exitCode = 1;
|
|
16604
|
+
return;
|
|
16605
|
+
}
|
|
16606
|
+
videoBytes = read;
|
|
16607
|
+
}
|
|
16608
|
+
const ctx = await context2(opts);
|
|
16609
|
+
if (!ctx) {
|
|
16610
|
+
log.error("Not authorized. Run `genex init` first to sign in.");
|
|
16611
|
+
process.exitCode = 1;
|
|
16612
|
+
return;
|
|
16613
|
+
}
|
|
16614
|
+
let videoUrl;
|
|
16615
|
+
if (opts.video && videoBytes) {
|
|
16616
|
+
const uploaded = await uploadVideo(ctx.apiUrl, ctx.token, characterId, opts.video, videoBytes, log);
|
|
16617
|
+
if (!uploaded) {
|
|
16618
|
+
process.exitCode = 1;
|
|
16619
|
+
return;
|
|
16620
|
+
}
|
|
16621
|
+
videoUrl = uploaded;
|
|
16622
|
+
}
|
|
16623
|
+
const response = await apiFetch(
|
|
16624
|
+
`${ctx.apiUrl}/api/characters/${encodeURIComponent(characterId)}/motions/plan`,
|
|
16625
|
+
{
|
|
16626
|
+
method: "POST",
|
|
16627
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${ctx.token}` },
|
|
16628
|
+
body: JSON.stringify({
|
|
16629
|
+
verbs,
|
|
16630
|
+
locomotion: Boolean(opts.locomotion),
|
|
16631
|
+
lean: Boolean(opts.lean),
|
|
16632
|
+
...videoUrl ? { videoUrl } : {},
|
|
16633
|
+
...opts.duration === void 0 ? {} : { seconds: opts.duration }
|
|
16634
|
+
})
|
|
16635
|
+
}
|
|
16636
|
+
);
|
|
16637
|
+
if (printedStructuredError(response)) {
|
|
16638
|
+
process.exitCode = 1;
|
|
16639
|
+
return;
|
|
16640
|
+
}
|
|
16641
|
+
if (!response.ok) {
|
|
16642
|
+
const body = await response.json().catch(() => ({}));
|
|
16643
|
+
log.error(body.message ?? `Couldn't plan those animations (HTTP ${response.status}).`);
|
|
16644
|
+
process.exitCode = 1;
|
|
16645
|
+
return;
|
|
16646
|
+
}
|
|
16647
|
+
const planned = await response.json();
|
|
16648
|
+
if (opts.json) {
|
|
16649
|
+
writeJson({ kind: "character_motion", stage: "plan", ...planned.plan });
|
|
16650
|
+
} else {
|
|
16651
|
+
printPlan(planned.plan, log, Boolean(opts.creature));
|
|
16652
|
+
}
|
|
16653
|
+
await runGenerate("character_motion", {
|
|
16654
|
+
...opts,
|
|
16655
|
+
prompt: verbs.length > 0 ? `Animate character ${characterId}: ${verbs.join(", ")}` : `Generate the locomotion set for character ${characterId}`,
|
|
16656
|
+
token: ctx.token,
|
|
16657
|
+
apiUrl: ctx.apiUrl,
|
|
16658
|
+
generationOptions: planned.request.options
|
|
16659
|
+
});
|
|
16660
|
+
}
|
|
16661
|
+
async function runCharacterMotions(opts) {
|
|
16662
|
+
const log = createLogger({ quiet: opts.quiet || opts.json });
|
|
16663
|
+
const characterId = opts.characterId?.trim();
|
|
16664
|
+
if (!characterId) {
|
|
16665
|
+
log.error("Missing character id.");
|
|
16666
|
+
process.exitCode = 1;
|
|
16667
|
+
return;
|
|
16668
|
+
}
|
|
16669
|
+
const ctx = await context2(opts);
|
|
16670
|
+
if (!ctx) {
|
|
16671
|
+
log.error("Not authorized. Run `genex init` first to sign in.");
|
|
16672
|
+
process.exitCode = 1;
|
|
16673
|
+
return;
|
|
16674
|
+
}
|
|
16675
|
+
const response = await apiFetch(
|
|
16676
|
+
`${ctx.apiUrl}/api/characters/${encodeURIComponent(characterId)}/manifest`,
|
|
16677
|
+
{ headers: { Authorization: `Bearer ${ctx.token}` } }
|
|
16678
|
+
);
|
|
16679
|
+
if (printedStructuredError(response)) {
|
|
16680
|
+
process.exitCode = 1;
|
|
16681
|
+
return;
|
|
16682
|
+
}
|
|
16683
|
+
if (!response.ok) {
|
|
16684
|
+
log.error(`Couldn't read that character (HTTP ${response.status}).`);
|
|
16685
|
+
process.exitCode = 1;
|
|
16686
|
+
return;
|
|
16687
|
+
}
|
|
16688
|
+
const { manifest } = await response.json();
|
|
16689
|
+
if (opts.json) {
|
|
16690
|
+
writeJson({
|
|
16691
|
+
kind: "character_motion",
|
|
16692
|
+
stage: "list",
|
|
16693
|
+
characterId,
|
|
16694
|
+
clips: manifest.clips.map((clip) => ({
|
|
16695
|
+
key: clip.key,
|
|
16696
|
+
name: clip.name,
|
|
16697
|
+
provider: clip.provider ?? "meshy",
|
|
16698
|
+
duration: clip.duration,
|
|
16699
|
+
slots: Object.entries(manifest.locomotion.slots).filter(([, value]) => value === clip.key).map(([slot]) => slot)
|
|
16700
|
+
}))
|
|
16701
|
+
});
|
|
16702
|
+
return;
|
|
16703
|
+
}
|
|
16704
|
+
log.plain(c.bold(`${manifest.clips.length} clips on ${characterId}`));
|
|
16705
|
+
for (const clip of manifest.clips) {
|
|
16706
|
+
const slots = Object.entries(manifest.locomotion.slots).filter(([, value]) => value === clip.key).map(([slot]) => slot);
|
|
16707
|
+
const where = slots.length > 0 ? ` \u2192 ${slots.join(", ")}` : "";
|
|
16708
|
+
const source = clip.provider === "uthana" ? c.dim(" generated") : "";
|
|
16709
|
+
log.plain(` ${clip.key}${source}${where}`);
|
|
16710
|
+
}
|
|
16711
|
+
}
|
|
16712
|
+
|
|
16266
16713
|
// src/commands/animations.ts
|
|
16267
16714
|
function animationSearchView(entry) {
|
|
16268
16715
|
return {
|
|
@@ -17978,20 +18425,26 @@ ${c.bold("Usage")}
|
|
|
17978
18425
|
genex character finalize <preview-id>
|
|
17979
18426
|
Finalize an approved preview as a 10,000-face
|
|
17980
18427
|
controller-ready Meshy character.
|
|
17981
|
-
genex character animate <id>
|
|
18428
|
+
genex character animate <id> "<verb>" \u2026
|
|
18429
|
+
Animate the character: say what it should DO,
|
|
18430
|
+
in plain words ("overhead slam"), one clip per
|
|
18431
|
+
verb. The platform picks how each is made and
|
|
18432
|
+
prints the plan before anything is charged.
|
|
18433
|
+
--locomotion generates the 8-way walk + run set;
|
|
18434
|
+
--video ./clip.mp4 uses your own footage;
|
|
18435
|
+
--duration 3-6 lengthens the reference video for
|
|
18436
|
+
multi-beat moves; --action <id-or-query> still
|
|
18437
|
+
picks a catalog clip.
|
|
18438
|
+
genex character motions <id> List the clips installed on that character.
|
|
17982
18439
|
genex creature "<desc>" One-shot rigged enemy/creature (biped-shaped
|
|
17983
18440
|
bodies only \u2014 Meshy rig limit): model \u2192 rig \u2192
|
|
17984
18441
|
bind library clips via --animation. No approval
|
|
17985
18442
|
ceremony, no player controller pack.
|
|
18443
|
+
genex creature animate <id> "<verb>" \u2026
|
|
18444
|
+
Same as character animate, for an enemy \u2014 a boss's
|
|
18445
|
+
signature move. --lean keeps movement to forward
|
|
18446
|
+
walk + run (enemies follow a path).
|
|
17986
18447
|
genex animations search "<intent>" Search Meshy's animation library by gameplay intent.
|
|
17987
|
-
genex motion <sub> [args] Custom character animations from text (ARDY):
|
|
17988
|
-
gen "<prompt>" (GPU service, bills per take) |
|
|
17989
|
-
verify <takes\u2026> | compile <takes\u2026> --out \u2026 |
|
|
17990
|
-
install [--set rifle] | constraints \u2014 the
|
|
17991
|
-
local toolkit + vendored runtime the agent
|
|
17992
|
-
owns. Catalog actions (genex animations
|
|
17993
|
-
search) stay the first stop; motion covers
|
|
17994
|
-
the verbs the catalog lacks.
|
|
17995
18448
|
genex wait <id> Attach to a generation enqueued with --no-wait and
|
|
17996
18449
|
print its asset URL(s) when it finishes. Safe to
|
|
17997
18450
|
re-run \u2014 it never creates a new generation.
|
|
@@ -18006,9 +18459,11 @@ ${c.bold("Usage")}
|
|
|
18006
18459
|
without a physics controller. "quality" = the
|
|
18007
18460
|
adaptive-quality kit (device tier, runtime governor,
|
|
18008
18461
|
per-tier asset rungs) every game wires at boot.
|
|
18009
|
-
Character
|
|
18462
|
+
Character's player body is the game's own generated
|
|
18463
|
+
character, with a VRM avatar as the fallback.
|
|
18010
18464
|
genex controller character --character <id>
|
|
18011
|
-
|
|
18465
|
+
Point the game at its generated character (writes the
|
|
18466
|
+
manifest; no code change \u2014 the loader picks it up).
|
|
18012
18467
|
genex controller anims <sel \u2026> Install legacy VRM + UAL animation clips by tag/name
|
|
18013
18468
|
into public/assets/anims/. --list shows the catalog;
|
|
18014
18469
|
--reset forgets the previous selection first.
|
|
@@ -18020,8 +18475,13 @@ ${c.bold("Usage")}
|
|
|
18020
18475
|
${c.bold("Options for the generators (`model` `skybox` `sfx` `music` `voice` `texture` `image` `video`)")}
|
|
18021
18476
|
--terrain (texture) seamless tiling surface for terrain/ground.
|
|
18022
18477
|
--raw (skybox) send the prompt exactly as written \u2014 skip the
|
|
18023
|
-
appended environment-only
|
|
18024
|
-
|
|
18478
|
+
appended environment-only clause AND the refusal on
|
|
18479
|
+
prompts that name structures/objects (a sky is sky only;
|
|
18480
|
+
landforms and weather always pass).
|
|
18481
|
+
--duration <sec> (sfx, video, music) target clip length in seconds;
|
|
18482
|
+
(character/creature animate) 3-6 s of reference video for
|
|
18483
|
+
verbs that route to AI video (default 3) \u2014 longer gives a
|
|
18484
|
+
multi-beat move room to land instead of rushing it.
|
|
18025
18485
|
(music: 10-300, default 90 \u2014 a looping gameplay track).
|
|
18026
18486
|
--transparent (image) transparent background (alpha) \u2014 for decals/stickers.
|
|
18027
18487
|
--glass (image) magenta-key glass lane: generate the panel on a
|
|
@@ -18136,15 +18596,13 @@ ${c.bold("Options for `character` / `animations search`")}
|
|
|
18136
18596
|
controller slots, requirements, review status, and estimated cost.
|
|
18137
18597
|
|
|
18138
18598
|
${c.bold("Options for `controller character` / legacy `controller anims`")}
|
|
18139
|
-
--character <id> (character) install the owned Meshy character manifest
|
|
18140
|
-
|
|
18599
|
+
--character <id> (character) install the owned Meshy character manifest \u2014 the
|
|
18600
|
+
game's own generated character becomes the player's body.
|
|
18141
18601
|
--list [selectors] (anims) show the bundled VRM + UAL tag/name catalog.
|
|
18142
18602
|
--reset (anims) forget the previous game-local UAL selection.
|
|
18143
18603
|
|
|
18144
18604
|
${UI_USAGE}
|
|
18145
18605
|
|
|
18146
|
-
${MOTION_USAGE}
|
|
18147
|
-
|
|
18148
18606
|
${c.bold("Global")}
|
|
18149
18607
|
--quiet Reduce output.
|
|
18150
18608
|
-h, --help Show this help.
|
|
@@ -18188,15 +18646,17 @@ ${c.bold("Examples")}
|
|
|
18188
18646
|
genex character finalize <preview-id> --user-approved --approve-remesh 10000 --animation 466
|
|
18189
18647
|
genex character "stylized sci-fi courier" --direct-text --no-wait
|
|
18190
18648
|
genex character animate <character-id> --action <action-id>
|
|
18649
|
+
genex character animate <character-id> "overhead slam" "parry and recover" --no-wait
|
|
18650
|
+
genex character animate <character-id> --locomotion --no-wait
|
|
18651
|
+
genex character animate <character-id> "leap the gap and roll on landing" --duration 6
|
|
18652
|
+
genex character animate <character-id> "victory pose" --video ./take-3.mp4
|
|
18653
|
+
genex character motions <character-id>
|
|
18191
18654
|
genex creature "hulking bone seraph, upright stance" --animation <walk-id> --animation <attack-id> --no-wait
|
|
18655
|
+
genex creature animate <creature-id> "wind-up ground pound" --no-wait
|
|
18192
18656
|
genex controller character --character <character-id>
|
|
18193
18657
|
genex controller anims sword pistol
|
|
18194
18658
|
genex controller touch
|
|
18195
18659
|
genex controller networked-physics
|
|
18196
|
-
genex motion gen "soldier rifle low-ready idle, fully upright, head held high" --takes 4 --no-wait
|
|
18197
|
-
genex motion verify takes/ --json
|
|
18198
|
-
genex motion compile takes/rifle-idle.npz takes/rifle-run-forward.npz --out src/motion/rifle.json
|
|
18199
|
-
genex motion install --set rifle
|
|
18200
18660
|
`;
|
|
18201
18661
|
function parseArgs(argv) {
|
|
18202
18662
|
const parsed = {
|
|
@@ -18266,7 +18726,8 @@ function parseArgs(argv) {
|
|
|
18266
18726
|
"--gates",
|
|
18267
18727
|
"--config",
|
|
18268
18728
|
"--set",
|
|
18269
|
-
"--speed"
|
|
18729
|
+
"--speed",
|
|
18730
|
+
"--video"
|
|
18270
18731
|
]);
|
|
18271
18732
|
let i = 0;
|
|
18272
18733
|
while (i < argv.length) {
|
|
@@ -18302,6 +18763,12 @@ function parseArgs(argv) {
|
|
|
18302
18763
|
case "--direct-text":
|
|
18303
18764
|
parsed.options.directText = true;
|
|
18304
18765
|
break;
|
|
18766
|
+
case "--locomotion":
|
|
18767
|
+
parsed.options.locomotion = true;
|
|
18768
|
+
break;
|
|
18769
|
+
case "--lean":
|
|
18770
|
+
parsed.options.lean = true;
|
|
18771
|
+
break;
|
|
18305
18772
|
case "--in-place":
|
|
18306
18773
|
parsed.options.inPlace = true;
|
|
18307
18774
|
break;
|
|
@@ -18323,6 +18790,9 @@ function parseArgs(argv) {
|
|
|
18323
18790
|
case "--no-defringe":
|
|
18324
18791
|
parsed.options.defringe = false;
|
|
18325
18792
|
break;
|
|
18793
|
+
case "--allow-speckle":
|
|
18794
|
+
parsed.options.allowSpeckle = true;
|
|
18795
|
+
break;
|
|
18326
18796
|
case "--loop":
|
|
18327
18797
|
parsed.options.loop = true;
|
|
18328
18798
|
break;
|
|
@@ -18379,20 +18849,27 @@ function parseArgs(argv) {
|
|
|
18379
18849
|
} else if (parsed.command === "controller") {
|
|
18380
18850
|
(parsed.options.selectors ??= []).push(arg);
|
|
18381
18851
|
} else if (parsed.command === "character") {
|
|
18382
|
-
if (parsed.options.name === "animate" && !parsed.options.characterId) {
|
|
18852
|
+
if ((parsed.options.name === "animate" || parsed.options.name === "motions") && !parsed.options.characterId) {
|
|
18383
18853
|
parsed.options.characterId = arg;
|
|
18854
|
+
} else if (parsed.options.name === "animate") {
|
|
18855
|
+
(parsed.options.verbs ??= []).push(arg);
|
|
18384
18856
|
} else if (parsed.options.name === "preview" && !parsed.options.conceptId) {
|
|
18385
18857
|
parsed.options.conceptId = arg;
|
|
18386
18858
|
} else if (parsed.options.name === "finalize" && !parsed.options.previewId) {
|
|
18387
18859
|
parsed.options.previewId = arg;
|
|
18388
|
-
} else if (!["animate", "preview", "finalize"].includes(parsed.options.name ?? "")) {
|
|
18860
|
+
} else if (!["animate", "motions", "preview", "finalize"].includes(parsed.options.name ?? "")) {
|
|
18389
18861
|
parsed.options.name = `${parsed.options.name} ${arg}`;
|
|
18390
18862
|
} else {
|
|
18391
18863
|
parsed.error = `Unexpected argument: ${arg}`;
|
|
18392
18864
|
return parsed;
|
|
18393
18865
|
}
|
|
18394
18866
|
} else if (parsed.command === "creature") {
|
|
18395
|
-
parsed.options.name
|
|
18867
|
+
if (parsed.options.name === "animate") {
|
|
18868
|
+
if (!parsed.options.characterId) parsed.options.characterId = arg;
|
|
18869
|
+
else (parsed.options.verbs ??= []).push(arg);
|
|
18870
|
+
} else {
|
|
18871
|
+
parsed.options.name = `${parsed.options.name} ${arg}`;
|
|
18872
|
+
}
|
|
18396
18873
|
} else if (parsed.command === "motion") {
|
|
18397
18874
|
if (parsed.options.name === "gen") {
|
|
18398
18875
|
parsed.options.query = parsed.options.query ? `${parsed.options.query} ${arg}` : arg;
|
|
@@ -18630,6 +19107,9 @@ function applyValueFlag(options, flag, value) {
|
|
|
18630
19107
|
case "--frame":
|
|
18631
19108
|
options.frameUrl = value;
|
|
18632
19109
|
break;
|
|
19110
|
+
case "--video":
|
|
19111
|
+
options.video = value;
|
|
19112
|
+
break;
|
|
18633
19113
|
case "--first-frame":
|
|
18634
19114
|
options.firstFrameUrl = value;
|
|
18635
19115
|
break;
|
|
@@ -18729,7 +19209,13 @@ async function main() {
|
|
|
18729
19209
|
break;
|
|
18730
19210
|
case "character":
|
|
18731
19211
|
if (parsed.options.name === "animate") {
|
|
18732
|
-
|
|
19212
|
+
if ((parsed.options.actions?.length ?? 0) > 0) {
|
|
19213
|
+
await runCharacterAnimate(parsed.options);
|
|
19214
|
+
} else {
|
|
19215
|
+
await runCharacterAnimateMotion(parsed.options);
|
|
19216
|
+
}
|
|
19217
|
+
} else if (parsed.options.name === "motions") {
|
|
19218
|
+
await runCharacterMotions(parsed.options);
|
|
18733
19219
|
} else if (parsed.options.name === "preview") {
|
|
18734
19220
|
await runCharacterPreview(parsed.options);
|
|
18735
19221
|
} else if (parsed.options.name === "finalize") {
|
|
@@ -18739,6 +19225,10 @@ async function main() {
|
|
|
18739
19225
|
}
|
|
18740
19226
|
break;
|
|
18741
19227
|
case "creature":
|
|
19228
|
+
if (parsed.options.name === "animate") {
|
|
19229
|
+
await runCharacterAnimateMotion({ ...parsed.options, creature: true });
|
|
19230
|
+
break;
|
|
19231
|
+
}
|
|
18742
19232
|
if (!parsed.options.json && !parsed.options.quiet) {
|
|
18743
19233
|
log.dim(
|
|
18744
19234
|
" Biped-shaped creatures only (Meshy rig limit) \u2014 for true quadrupeds/fliers use a static model + the genex-threejs-creatures procedural recipes."
|
|
@@ -18771,6 +19261,23 @@ async function main() {
|
|
|
18771
19261
|
case "ui":
|
|
18772
19262
|
await runUi({ ...parsed.options, sub: parsed.options.name });
|
|
18773
19263
|
break;
|
|
19264
|
+
// PARKED (AG-824 D12) — still dispatches, deliberately unlisted in --help
|
|
19265
|
+
// and gone from the skills, the same way `genex explore` is parked.
|
|
19266
|
+
//
|
|
19267
|
+
// Why: `genex character animate` replaced it. That lane generates motion
|
|
19268
|
+
// that binds to the character's OWN rig with no retargeting (measured: the
|
|
19269
|
+
// provider returns our exact skeleton), which is what the ~1,700 lines of
|
|
19270
|
+
// local retargeting behind this command existed to solve.
|
|
19271
|
+
//
|
|
19272
|
+
// Everything under it is intact and dormant: src/commands/motion.ts,
|
|
19273
|
+
// src/lib/motion/**, templates/motion/**, the API's runpod-motion provider
|
|
19274
|
+
// and its `motion` kind, and the RunPod worker (idle cost measured at $0).
|
|
19275
|
+
//
|
|
19276
|
+
// To re-enable: restore MOTION_USAGE + the examples in this file and the
|
|
19277
|
+
// skill cross-links. The trap that already cost time —
|
|
19278
|
+
// RUNPOD_MOTION_ENDPOINT_ID must be a GitHub Environment `vars.` entry,
|
|
19279
|
+
// NOT a secret of the same name: a same-named secret upserts EMPTY, and
|
|
19280
|
+
// the provider then silently falls back to the mock with no error anywhere.
|
|
18774
19281
|
case "motion":
|
|
18775
19282
|
await runMotion({ ...parsed.options, sub: parsed.options.name });
|
|
18776
19283
|
break;
|