@genex-ai/cli-demo 1.33.0-dev.690 → 1.34.0-dev.692

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1925,7 +1925,8 @@ async function readLedger(cwd = process.cwd()) {
1925
1925
  prompt: typeof e.prompt === "string" ? e.prompt : "",
1926
1926
  ...typeof e.tail === "string" ? { promptTail: e.tail } : {},
1927
1927
  queuedAt: typeof e.at === "string" ? e.at : "",
1928
- status: "queued"
1928
+ status: "queued",
1929
+ ...typeof e.out === "string" && e.out ? { outDir: e.out } : {}
1929
1930
  });
1930
1931
  }
1931
1932
  } else if (e.t === "done" && typeof e.id === "string") {
@@ -1949,7 +1950,8 @@ async function recordQueued(id, kind, prompt, cwd = process.cwd(), extra = {}) {
1949
1950
  prompt: prompt.slice(0, 120),
1950
1951
  ...prompt.length > 120 ? { tail: prompt.slice(-120) } : {},
1951
1952
  at: (/* @__PURE__ */ new Date()).toISOString(),
1952
- ...typeof credits === "number" && Number.isFinite(credits) && credits >= 0 ? { credits } : {}
1953
+ ...typeof credits === "number" && Number.isFinite(credits) && credits >= 0 ? { credits } : {},
1954
+ ...typeof extra.outDir === "string" && extra.outDir.trim() ? { out: extra.outDir.trim() } : {}
1953
1955
  });
1954
1956
  }
1955
1957
  async function recordTerminal(id, status, urls, cwd = process.cwd()) {
@@ -2018,9 +2020,10 @@ function localAssetStem(kind, prompt, id) {
2018
2020
  }
2019
2021
  function localAssetName(kind, prompt, id, file, multi) {
2020
2022
  const base = localAssetStem(kind, prompt, id);
2021
- const role = multi ? `-${file.role.replace(/^(texture|image|character|model|model_anim|model-anim)-/, "")}` : "";
2023
+ const label = file.label ? `-${file.label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60)}` : "";
2024
+ const role = multi && !label ? `-${file.role.replace(/^(texture|image|character|model|model_anim|model-anim)-/, "")}` : "";
2022
2025
  const ext = file.ext.replace(/^\./, "") || "bin";
2023
- return `${base}${role}.${ext}`;
2026
+ return `${base}${label}${role}.${ext}`;
2024
2027
  }
2025
2028
  async function downloadAssets(files, opts) {
2026
2029
  const wanted = primaryFiles(files);
@@ -7981,6 +7984,15 @@ function oneLineError(error, max = RAW_ERROR_MAX_CHARS) {
7981
7984
  }
7982
7985
  function describeGenerationFailure(kind, error) {
7983
7986
  if (!isProviderBillingError(error)) {
7987
+ if (error && /both failed native controller QA/.test(error)) {
7988
+ return {
7989
+ billing: false,
7990
+ lines: [
7991
+ c.red(error),
7992
+ ` For a rigged body from any mesh: ${c.cyan('npx genex model "<prompt>" --face-limit 20000')} then ${c.cyan("npx genex character import <that .glb>")} \u2014 Uthana auto-rigs it, and \`character animate\` gives it every move.`
7993
+ ]
7994
+ };
7995
+ }
7984
7996
  return { billing: false, lines: error ? [c.red(error)] : [] };
7985
7997
  }
7986
7998
  return {
@@ -8021,11 +8033,16 @@ function unshippedCounts(rows) {
8021
8033
  break;
8022
8034
  }
8023
8035
  }
8036
+ const failed = /* @__PURE__ */ new Set();
8037
+ for (const r of rows) {
8038
+ if (r?.t === "done" && r.status === "failed" && typeof r.id === "string") failed.add(r.id);
8039
+ }
8024
8040
  let total = 0;
8025
8041
  let character = 0;
8026
8042
  for (let i = lastShip + 1; i < rows.length; i += 1) {
8027
8043
  const r = rows[i];
8028
8044
  if (r?.t !== "q") continue;
8045
+ if (typeof r.id === "string" && failed.has(r.id)) continue;
8029
8046
  total += 1;
8030
8047
  if (typeof r.kind === "string" && CHARACTER_KINDS.has(r.kind)) character += 1;
8031
8048
  }
@@ -8737,7 +8754,10 @@ async function runGenerate(kind, opts) {
8737
8754
  process.exitCode = 1;
8738
8755
  return;
8739
8756
  }
8740
- await recordQueued(id, kind, prompt, void 0, { credits: creditsQuoted ?? opts.quotedCredits });
8757
+ await recordQueued(id, kind, prompt, void 0, {
8758
+ credits: creditsQuoted ?? opts.quotedCredits,
8759
+ ...opts.outDir ? { outDir: opts.outDir } : {}
8760
+ });
8741
8761
  if (opts.noWait) {
8742
8762
  if (opts.json) {
8743
8763
  writeJson({
@@ -8862,14 +8882,18 @@ async function reportGlassTerminal(view, outDir, log, json) {
8862
8882
  }
8863
8883
  }
8864
8884
  async function awaitAndReport(apiUrl, token, id, kind, log, open = false, json = false, local) {
8865
- if (!json) log.step("Generating\u2026 (this can take up to a minute)");
8885
+ if (!json) log.step(waitCopy(kind));
8886
+ const started = Date.now();
8866
8887
  const view = await waitForTerminalView(
8867
8888
  apiUrl,
8868
8889
  token,
8869
8890
  id,
8870
8891
  kind,
8871
8892
  json ? () => {
8872
- } : (p) => log.dim(` ${p}%`)
8893
+ } : (p) => {
8894
+ const minutes = Math.floor((Date.now() - started) / 6e4);
8895
+ log.dim(` ${p}%${minutes >= 1 ? ` (${minutes} min)` : ""}`);
8896
+ }
8873
8897
  );
8874
8898
  if (!view) {
8875
8899
  if (json) writeJson({ kind, id, status: "failed", error: "Timed out waiting for the generation." });
@@ -8879,6 +8903,25 @@ async function awaitAndReport(apiUrl, token, id, kind, log, open = false, json =
8879
8903
  }
8880
8904
  await reportTerminal(kind, view, log, open, json, local);
8881
8905
  }
8906
+ function waitCopy(kind) {
8907
+ switch (kind) {
8908
+ case "character_motion":
8909
+ return "Generating motion clips\u2026 (each clip is its own provider job \u2014 a batch takes several minutes, a 16-clip walk + run set ten or more; keep building and check back with genex wait --all)";
8910
+ case "character":
8911
+ case "character_concept":
8912
+ case "character_preview":
8913
+ return "Generating\u2026 (character stages take several minutes server-side)";
8914
+ case "model":
8915
+ case "model_segment":
8916
+ case "model_rig":
8917
+ case "model_animation":
8918
+ return "Generating\u2026 (a mesh takes a few minutes; --low-poly up to thirty)";
8919
+ case "video":
8920
+ return "Generating\u2026 (a clip takes a minute or two)";
8921
+ default:
8922
+ return "Generating\u2026 (this can take up to a minute)";
8923
+ }
8924
+ }
8882
8925
  async function waitForTerminalView(apiUrl, token, id, kind, onProgress = () => {
8883
8926
  }) {
8884
8927
  const timeoutMs = waitTimeoutFor(kind);
@@ -8934,6 +8977,7 @@ async function reportTerminal(kind, view, log, open = false, json = false, local
8934
8977
  if (!json) reportMocked(view, log);
8935
8978
  return;
8936
8979
  }
8980
+ const batch = motionBatchOutcome(view);
8937
8981
  if (json) {
8938
8982
  writeJson({
8939
8983
  kind,
@@ -8945,12 +8989,25 @@ async function reportTerminal(kind, view, log, open = false, json = false, local
8945
8989
  ...delivered ? { saved: delivered.saved, downloadFailures: delivered.failures } : {},
8946
8990
  ...kind === "character" ? {
8947
8991
  characterId: view.id,
8948
- nextCommand: `genex controller character --character ${view.id}`
8992
+ nextCommand: `genex controller character --character ${view.id}`,
8993
+ ...rigFallbackOf(view) ? { rigProvider: "uthana", rigFallback: rigFallbackOf(view) } : {}
8994
+ } : {},
8995
+ // A motion batch that lost a clip is `completed` (what landed is real)
8996
+ // AND `partial: true` — the flag a machine reader must check.
8997
+ ...batch ? {
8998
+ characterId: batch.characterId,
8999
+ partial: batch.partial,
9000
+ clipsRequested: batch.requested,
9001
+ clipsInstalled: batch.installed,
9002
+ ...batch.failures.length > 0 ? { failures: batch.failures } : {},
9003
+ ...batch.fallbacks.length > 0 ? { fallbacks: batch.fallbacks } : {},
9004
+ ...batch.refundedCredits ? { refundedCredits: batch.refundedCredits } : {}
8949
9005
  } : {}
8950
9006
  });
8951
9007
  return;
8952
9008
  }
8953
9009
  log.plain("");
9010
+ if (batch?.partial) reportPartialBatch(batch, log);
8954
9011
  if (delivered) {
8955
9012
  reportLocalFiles(delivered, log, false);
8956
9013
  } else {
@@ -8977,8 +9034,70 @@ async function reportTerminal(kind, view, log, open = false, json = false, local
8977
9034
  log.plain(` ${files[0].url}`);
8978
9035
  log.plain("");
8979
9036
  }
9037
+ if (batch && !batch.partial && batch.fallbacks.length > 0) reportFallbacks(batch, log);
9038
+ if (kind === "character") {
9039
+ const fallback = rigFallbackOf(view);
9040
+ if (fallback) {
9041
+ log.plain(
9042
+ ` ${c.yellow("\u26A0")} Meshy's rig failed pose QA in both rest poses (${fallback.poseMode} mesh kept), so ${c.bold("Uthana auto-rigged it instead")}: no controller pack and no catalog clips on this body. Give it moves with ${c.cyan(`genex character animate ${view.id} --locomotion`)} and plain-word verbs.`
9043
+ );
9044
+ }
9045
+ }
8980
9046
  printHint(kind, view, files, log, delivered !== null);
8981
9047
  }
9048
+ function motionBatchOutcome(view) {
9049
+ if (view.kind !== "character_motion" && view.kind !== void 0) return null;
9050
+ const meta = metadataOf(view);
9051
+ const requested = typeof meta.clipsRequested === "number" ? meta.clipsRequested : void 0;
9052
+ const installed = typeof meta.clipsInstalled === "number" ? meta.clipsInstalled : void 0;
9053
+ if (requested === void 0 || installed === void 0) return null;
9054
+ const failures = Array.isArray(meta.failures) ? meta.failures.filter((f) => typeof f === "object" && f !== null && typeof f.clip === "string").map((f) => ({ clip: f.clip, error: typeof f.error === "string" ? f.error : "" })) : [];
9055
+ const fallbacks = Array.isArray(meta.fallbacks) ? meta.fallbacks.filter((f) => typeof f === "object" && f !== null && typeof f.clip === "string") : [];
9056
+ return {
9057
+ ...typeof meta.characterId === "string" ? { characterId: meta.characterId } : {},
9058
+ // Servers before 2026-09-13 sent no `outcome`; a non-empty failure list
9059
+ // says the same thing.
9060
+ partial: meta.outcome === "partial" || failures.length > 0,
9061
+ requested,
9062
+ installed,
9063
+ failures,
9064
+ fallbacks,
9065
+ ...typeof meta.refundedCredits === "number" ? { refundedCredits: meta.refundedCredits } : {}
9066
+ };
9067
+ }
9068
+ function reportPartialBatch(batch, log) {
9069
+ const missing = batch.requested - batch.installed;
9070
+ log.plain(
9071
+ `${c.yellow("\u26A0")} ${c.bold(`${batch.installed} of ${batch.requested} clips landed`)} \u2014 ${missing} did not:`
9072
+ );
9073
+ for (const f of batch.failures) {
9074
+ log.plain(` \xB7 ${c.bold(f.clip)}${f.error ? c.dim(` \u2014 ${oneLineError(f.error)}`) : ""}`);
9075
+ }
9076
+ const verbs = batch.failures.map((f) => `"${f.clip}"`).join(" ");
9077
+ if (batch.characterId && verbs) {
9078
+ log.plain(` Re-run only the missing ones: ${c.cyan(`genex character animate ${batch.characterId} ${verbs}`)}`);
9079
+ }
9080
+ if (batch.refundedCredits) log.dim(` ${batch.refundedCredits} credits refunded for what did not land.`);
9081
+ if (batch.fallbacks.length > 0) reportFallbacks(batch, log);
9082
+ log.plain("");
9083
+ }
9084
+ function reportFallbacks(batch, log) {
9085
+ for (const f of batch.fallbacks) {
9086
+ log.plain(
9087
+ ` ${c.yellow("\u21B3")} ${c.bold(f.clip)} was ${f.to === "text" ? "described" : f.to} instead of ${f.from === "video" ? "filmed" : f.from}${f.reason ? c.dim(` (${oneLineError(f.reason)})`) : ""} \u2014 watch it on the character before calling it done.`
9088
+ );
9089
+ }
9090
+ }
9091
+ function rigFallbackOf(view) {
9092
+ const meta = metadataOf(view);
9093
+ const fallback = meta.rigFallback;
9094
+ if (!fallback || typeof fallback !== "object") return null;
9095
+ const { poseMode, failures } = fallback;
9096
+ return {
9097
+ poseMode: typeof poseMode === "string" ? poseMode : "a-pose",
9098
+ failures: Array.isArray(failures) ? failures.filter((f) => typeof f === "string") : []
9099
+ };
9100
+ }
8982
9101
  function reportLocalFiles(result, log, json) {
8983
9102
  if (json) return;
8984
9103
  if (result.saved.length > 0) {
@@ -9161,7 +9280,9 @@ function assetHint(kind, view, url) {
9161
9280
  character_preview: "Show all four preview views to the user and wait for explicit approval before finalizing.",
9162
9281
  character: `Install the controller and current manifest with genex controller character --character ${view.id}.`,
9163
9282
  character_animation: "The action is now part of the character's current manifest; refresh the local controller manifest before testing.",
9164
- 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.`,
9283
+ // The CHARACTER's id, not this motion job's: the hint printed the job id
9284
+ // for four months and the command it named did not resolve (2026-09-13).
9285
+ character_motion: `The clips are part of the character's current manifest \u2014 install them with genex controller character --character ${stringValue(metadataOf(view).characterId) ?? "<character-id>"}, then play the game and watch the motion on the real character before calling it done.`,
9165
9286
  model_segment: `One GLB with NAMED parts \u2014 load it through the quality kit (loadModelWithFallback, as for a plain model), then getObjectByName / traverse scene.children to move, detach, or swap a part. Part names print above when the provider reports them.`,
9166
9287
  model_rig: `Rigged GLB (Tripo skeleton) \u2014 load it through the quality kit (loadModelWithFallback); give it motion with genex model animate ${view.id} --preset walk, or drive the bones in code.`,
9167
9288
  model_animation: `Animated GLB \u2014 load it through the quality kit (loadModelWithFallback) and play its clips via THREE.AnimationMixer (gltf.animations).`
@@ -9590,8 +9711,12 @@ async function runWait(opts) {
9590
9711
  return;
9591
9712
  }
9592
9713
  const kind = view.kind ?? "image";
9593
- const ledgerPrompt = (await readLedger(opts.cwd ?? process.cwd())).find((e) => e.id === id)?.prompt;
9594
- const local = localDeliveryFor(await workspaceMode(), opts, ledgerPrompt || kind);
9714
+ const ledgerEntry = (await readLedger(opts.cwd ?? process.cwd())).find((e) => e.id === id);
9715
+ const local = localDeliveryFor(
9716
+ await workspaceMode(),
9717
+ { ...opts, outDir: opts.outDir ?? ledgerEntry?.outDir },
9718
+ ledgerEntry?.prompt || kind
9719
+ );
9595
9720
  log.plain(c.bold("genex wait"));
9596
9721
  log.dim(` ${kind} ${id}`);
9597
9722
  log.plain("");
@@ -9603,6 +9728,10 @@ async function runWait(opts) {
9603
9728
  log.dim(
9604
9729
  ` \u23F3 Character stages take several minutes server-side \u2014 you don't have to sit here. Keep building and check back with ${c.cyan("genex wait --all")}.`
9605
9730
  );
9731
+ } else if (kind === "character_motion") {
9732
+ log.dim(
9733
+ ` \u23F3 Motion batches take several minutes server-side (each clip is its own job; a 16-clip walk + run set ten or more) \u2014 you don't have to sit here. Keep building and check back with ${c.cyan("genex wait --all")}.`
9734
+ );
9606
9735
  }
9607
9736
  await awaitAndReport(apiUrl, token, id, kind, log, opts.open, opts.json, local);
9608
9737
  }
@@ -9667,7 +9796,7 @@ async function runWaitAll(opts) {
9667
9796
  for (const e of ledger) {
9668
9797
  const v = views.get(e.id);
9669
9798
  if (v?.status !== "completed" || !v.files?.length) continue;
9670
- const local = localDeliveryFor("tools", opts, e.prompt || e.kind);
9799
+ const local = localDeliveryFor("tools", { ...opts, outDir: opts.outDir ?? e.outDir }, e.prompt || e.kind);
9671
9800
  if (!local) continue;
9672
9801
  const outDir = path25.join(path25.relative(process.cwd(), cwd) || ".", local.outDir);
9673
9802
  const target = { kind: e.kind, prompt: local.prompt, id: e.id, outDir };
@@ -9687,21 +9816,27 @@ async function runWaitAll(opts) {
9687
9816
  log.plain(c.bold("genex wait --all"));
9688
9817
  log.plain("");
9689
9818
  for (const r of rows) {
9690
- const mark = r.status === "done" ? c.green("\u2713") : r.status === "failed" ? c.red("\u2717") : c.yellow("\u2026");
9691
- const label = r.status === "running" && typeof r.progress === "number" ? `running ${r.progress}%` : r.status;
9819
+ const mark = r.partial ? c.yellow("\u26A0") : r.status === "done" ? c.green("\u2713") : r.status === "failed" ? c.red("\u2717") : c.yellow("\u2026");
9820
+ const label = r.status === "running" && typeof r.progress === "number" ? `running ${r.progress}%` : r.partial ? "partial" : r.status;
9692
9821
  log.plain(` ${mark} ${label.padEnd(12)} ${r.kind.padEnd(8)} ${r.id} ${c.dim(`"${r.prompt.slice(0, 56)}"`)}`);
9822
+ if (r.partial) {
9823
+ log.plain(
9824
+ ` ${c.yellow(`${r.partial.installed} of ${r.partial.requested} clips landed`)} \u2014 missing: ${r.partial.failed.join(", ")}. ${c.dim(`genex wait ${r.id}`)} names the fix.`
9825
+ );
9826
+ }
9693
9827
  if (r.status === "failed" && r.error) {
9694
9828
  for (const line of describeGenerationFailure(r.kind, r.error).lines) log.plain(` ${line}`);
9695
9829
  }
9696
9830
  if (r.status === "done" && r.mocked) log.dim(` ${MOCKED_LINE}`);
9697
9831
  }
9698
9832
  const count = (s) => rows.filter((r) => r.status === s).length;
9833
+ const partial = rows.filter((r) => r.partial).length;
9699
9834
  const billingFailed = rows.filter(
9700
9835
  (r) => r.status === "failed" && isProviderBillingError(r.error)
9701
9836
  ).length;
9702
9837
  log.plain("");
9703
9838
  log.plain(
9704
- ` ${count("done")} done \xB7 ${count("running")} running \xB7 ${count("queued")} queued \xB7 ${count("failed")} failed`
9839
+ ` ${count("done")} done${partial > 0 ? ` (${partial} partial)` : ""} \xB7 ${count("running")} running \xB7 ${count("queued")} queued \xB7 ${count("failed")} failed`
9705
9840
  );
9706
9841
  if (billingFailed > 0) {
9707
9842
  log.dim(
@@ -9750,8 +9885,15 @@ async function toRow(e, v, cwd) {
9750
9885
  });
9751
9886
  }
9752
9887
  switch (v.status) {
9753
- case "completed":
9754
- return { ...base, status: "done", ...v.mocked === true ? { mocked: true } : {} };
9888
+ case "completed": {
9889
+ const batch = motionBatchOutcome(v);
9890
+ return {
9891
+ ...base,
9892
+ status: "done",
9893
+ ...v.mocked === true ? { mocked: true } : {},
9894
+ ...batch?.partial ? { partial: { installed: batch.installed, requested: batch.requested, failed: batch.failures.map((f) => f.clip) } } : {}
9895
+ };
9896
+ }
9755
9897
  case "failed":
9756
9898
  return { ...base, status: "failed", error: v.error };
9757
9899
  case "processing":
@@ -19952,7 +20094,7 @@ async function postWorkflow(url, token, body) {
19952
20094
  }
19953
20095
  async function runWorkflow(args) {
19954
20096
  const log = createLogger({ quiet: args.opts.quiet || args.opts.json });
19955
- {
20097
+ if (await workspaceMode() !== "tools") {
19956
20098
  const counts = unshippedCounts(await readLedgerRows());
19957
20099
  const verdict = ceilingVerdict({ kind: args.kind, counts, config: ceilingConfig() });
19958
20100
  if (!verdict.allow) {
@@ -19981,7 +20123,8 @@ async function runWorkflow(args) {
19981
20123
  return;
19982
20124
  }
19983
20125
  await recordQueued(created.id, args.kind, args.prompt, void 0, {
19984
- credits: created.creditsQuoted ?? args.quotedCredits
20126
+ credits: created.creditsQuoted ?? args.quotedCredits,
20127
+ ...args.opts.outDir ? { outDir: args.opts.outDir } : {}
19985
20128
  });
19986
20129
  if (args.opts.noWait) {
19987
20130
  const nextCommand = `genex wait ${created.id}${args.opts.json ? " --json" : ""}`;
@@ -20376,7 +20519,7 @@ async function runCharacterAnimate(opts) {
20376
20519
  // src/commands/character-motion.ts
20377
20520
  import { readFile } from "fs/promises";
20378
20521
  import { basename } from "path";
20379
- var VIDEO_SECONDS = [3, 4, 5, 6];
20522
+ var VIDEO_SECONDS = [5, 6, 7, 8];
20380
20523
  var MAX_VIDEO_BYTES = 200 * 1024 * 1024;
20381
20524
  async function context2(opts) {
20382
20525
  const token = opts.token ?? await readUserToken(opts.envPath);
@@ -20569,28 +20712,44 @@ async function runCharacterMotions(opts) {
20569
20712
  return;
20570
20713
  }
20571
20714
  const { manifest } = await response.json();
20715
+ const isMovementSlot = (slot) => /^(walk|run|crouch)\.[a-z-]+$/.test(slot);
20716
+ const slotsOf = (key) => Object.entries(manifest.locomotion.slots).filter(([, value]) => value === key).map(([slot]) => slot);
20572
20717
  if (opts.json) {
20573
20718
  writeJson({
20574
20719
  kind: "character_motion",
20575
20720
  stage: "list",
20576
20721
  characterId,
20577
- clips: manifest.clips.map((clip) => ({
20578
- key: clip.key,
20579
- name: clip.name,
20580
- provider: clip.provider ?? "meshy",
20581
- duration: clip.duration,
20582
- slots: Object.entries(manifest.locomotion.slots).filter(([, value]) => value === clip.key).map(([slot]) => slot)
20583
- }))
20722
+ ...manifest.rig ? { rig: manifest.rig } : {},
20723
+ // Additive since 1.34: the body's URL and the per-clip URL the manifest
20724
+ // already carried, so a local downloader needs no second endpoint.
20725
+ ...manifest.model?.url ? { modelUrl: manifest.model.url } : {},
20726
+ ...manifest.model?.skeletonSignature ? { skeletonSignature: manifest.model.skeletonSignature } : {},
20727
+ clips: manifest.clips.map((clip) => {
20728
+ const slots = slotsOf(clip.key);
20729
+ return {
20730
+ key: clip.key,
20731
+ name: clip.name,
20732
+ provider: clip.provider ?? "meshy",
20733
+ duration: clip.duration,
20734
+ ...clip.url ? { url: clip.url } : {},
20735
+ ...typeof clip.loop === "boolean" ? { loop: clip.loop } : {},
20736
+ ...typeof clip.nominalSpeed === "number" ? { nominalSpeed: clip.nominalSpeed } : {},
20737
+ slots,
20738
+ role: slots.some(isMovementSlot) ? "locomotion" : "action"
20739
+ };
20740
+ })
20584
20741
  });
20585
20742
  return;
20586
20743
  }
20587
20744
  log.plain(c.bold(`${manifest.clips.length} clips on ${characterId}`));
20588
20745
  for (const clip of manifest.clips) {
20589
- const slots = Object.entries(manifest.locomotion.slots).filter(([, value]) => value === clip.key).map(([slot]) => slot);
20746
+ const slots = slotsOf(clip.key);
20590
20747
  const where = slots.length > 0 ? ` \u2192 ${slots.join(", ")}` : "";
20591
20748
  const source = clip.provider === "uthana" ? c.dim(" generated") : "";
20592
- log.plain(` ${clip.key}${source}${where}`);
20749
+ const role = slots.some(isMovementSlot) ? "" : c.dim(" action");
20750
+ log.plain(` ${clip.key}${source}${role}${where}`);
20593
20751
  }
20752
+ log.dim(` Per-clip URLs and the body's URL: ${c.cyan(`genex character motions ${characterId} --json`)}`);
20594
20753
  }
20595
20754
 
20596
20755
  // src/commands/animations.ts
@@ -23388,6 +23547,7 @@ var TYPICAL_CREDITS = {
23388
23547
  character_import: 18
23389
23548
  // Uthana auto-rig of your own GLB; the upload itself is free
23390
23549
  };
23550
+ var CREATURE_TYPICAL_CREDITS = 46;
23391
23551
 
23392
23552
  // src/index.ts
23393
23553
  var TEXTURE_FLAG_VALUES = ["standard", "detailed", "none", "2k", "4k", "8k"];
@@ -23524,7 +23684,7 @@ ${c.bold("Usage")}
23524
23684
  prints the plan before anything is charged.
23525
23685
  --locomotion generates the 8-way walk + run set;
23526
23686
  --video ./clip.mp4 uses your own footage;
23527
- --duration 3-6 lengthens the reference video for
23687
+ --duration 5-8 lengthens the reference video for
23528
23688
  multi-beat moves; --action <id-or-query> still
23529
23689
  picks a catalog clip.
23530
23690
  genex character motions <id> List the clips installed on that character.
@@ -23620,8 +23780,8 @@ ${c.bold("Options for the generators (`model` `sfx` `music` `voice` `texture` `i
23620
23780
  per-plan lists above); each clip is billed.
23621
23781
  --terrain (texture) seamless tiling surface for terrain/ground.
23622
23782
  --duration <sec> (sfx, video, music) target clip length in seconds;
23623
- (character/creature animate) 3-6 s of reference video for
23624
- verbs that route to AI video (default 3) \u2014 longer gives a
23783
+ (character/creature animate) 5-8 s of reference video for
23784
+ verbs that route to AI video (default 5) \u2014 longer gives a
23625
23785
  multi-beat move room to land instead of rushing it.
23626
23786
  (music: 10-300, default 90 \u2014 a looping gameplay track).
23627
23787
  --transparent (image) transparent background (alpha) \u2014 for decals/stickers.
@@ -23901,11 +24061,14 @@ ${c.bold("Characters")}
23901
24061
  The rigged, game-ready character. ${cr("character_finalize")}
23902
24062
  genex character "<brief>" --direct-text
23903
24063
  One shot, no approval steps. ${cr("character")}
23904
- genex creature "<desc>" A rigged enemy (biped-shaped bodies). ${cr("character")}
24064
+ genex creature "<desc>" A rigged enemy (biped-shaped bodies). ${String(CREATURE_TYPICAL_CREDITS).padStart(3)} cr
24065
+ genex character import <file.glb> Your own humanoid mesh (or a genex model), ${cr("character_import")}
24066
+ auto-rigged by Uthana; the upload is free.
23905
24067
  genex character animate <id> "<verb>" \u2026
23906
24068
  Any move, in plain words. --locomotion ${cr("character_motion")} each
23907
- for the 8-way walk+run set; --video
23908
- ./clip.mp4 to use your own footage.
24069
+ for the 8-way walk+run set ("walk forward"
24070
+ is one clip); --video ./clip.mp4 to use
24071
+ your own footage.
23909
24072
  genex character motions <id> List the clips installed on a character.
23910
24073
  genex animations search "<intent>" Search the ready-made animation library. free
23911
24074
 
@@ -23949,6 +24112,74 @@ Hosting, publishing, multiplayer, remixing and custom domains are the Genex
23949
24112
  platform \u2014 build a game there from one prompt: ${c.cyan("https://genex.games")}
23950
24113
  `;
23951
24114
  var COMMAND_HELP = {
24115
+ character: `${c.bold("genex character")} \u2014 a rigged humanoid body, and every move it makes.
24116
+
24117
+ ${c.bold("Make one")}
24118
+ genex character "<brief>" Three concept candidates (guided flow, step 1).
24119
+ genex character preview <concept-id> --candidate <1|2|3> --user-approved
24120
+ A 3D preview of the chosen one (step 2).
24121
+ genex character finalize <preview-id> --user-approved --approve-remesh <faces>
24122
+ The rigged, game-ready character (step 3).
24123
+ genex character "<brief>" --direct-text One shot, no approval steps.
24124
+ genex creature "<desc>" The one-shot lane with enemy defaults (no controller pack).
24125
+ genex character import <file.glb> [--height <m>] [--no-fingers]
24126
+ Your own humanoid mesh \u2014 or a \`genex model\` GLB \u2014
24127
+ auto-rigged by Uthana. T- or A-pose, feet on the
24128
+ ground, facing +Z, .glb only. The upload is free.
24129
+ The result takes verbs, --locomotion and --video;
24130
+ the Meshy catalog and controller pack do not apply.
24131
+ genex character adopt [<id>] (game workspaces) claim a remixed game's character.
24132
+
24133
+ ${c.bold("Knobs (one shot / preview / finalize)")}
24134
+ --polycount <faces> \xB7 --approve-remesh <faces> 10000-100000 (10k crowds, 20-30k a player body).
24135
+ --texture 2k|4k|8k \xB7 --no-ultra \xB7 --pose a-pose|t-pose \xB7 --height <metres>
24136
+
24137
+ ${c.bold("Animate it")}
24138
+ genex character animate <id> "<verb>" ["<verb>" \u2026]
24139
+ One clip per verb, in plain words. The server
24140
+ prints the plan (route and clip count per verb)
24141
+ before anything is charged.
24142
+ genex character animate <id> --locomotion The 8-way walk + run set (16 clips).
24143
+ genex character animate <id> "walk forward" One movement clip: <walk|run> [direction].
24144
+ "walk" alone is the 8-direction walk set.
24145
+ --video ./clip.mp4 Use your own footage for that one verb.
24146
+ --duration 5-8 Reference-video length for verbs the platform
24147
+ films first (default 5).
24148
+ --action <id-or-query> A ready-made library clip instead (Meshy rigs).
24149
+ genex character motions <id> [--json] What is installed: key, verb, slot, role; --json
24150
+ adds the per-clip and body URLs.
24151
+ genex animations search "<intent>" The ready-made library. Free.
24152
+
24153
+ ${c.bold("When a batch does not fully land")}
24154
+ A motion batch that lost a clip completes with what landed and says so \u2014 "22 of 23
24155
+ clips landed \u2014 missing: \u2026" with the exact re-run command; --json carries partial:true.
24156
+ A verb the video model refuses is described instead of filmed, and the batch says which.
24157
+ A Meshy rig refused by pose QA in both rest poses is auto-rigged by Uthana instead: no
24158
+ controller pack, so give it moves with \`character animate <id> --locomotion\`.
24159
+ `,
24160
+ model: `${c.bold("genex model")} \u2014 a 3D model (GLB), and the mesh lanes on top of one.
24161
+
24162
+ ${c.bold("Usage")}
24163
+ genex model "<prompt>" [--image <path|url>] A GLB from text, or built FROM a reference image.
24164
+ genex model import <file.glb> A mesh you already have becomes a model of yours.
24165
+ Free; .glb only, \u2264 64 MB. Then segment / rig /
24166
+ animate / character import take its id.
24167
+ genex model segment <id> [--granularity simple|balanced|detailed]
24168
+ One GLB split into named parts (tripo_part_N).
24169
+ genex model rig <id> [--type biped|quadruped|hexapod|octopod|avian|serpentine|aquatic]
24170
+ A skeleton for any mesh; the plan is auto-detected.
24171
+ genex model animate <rig-id> --preset walk[,run,\u2026]
24172
+ Ready-made clips retargeted onto that rig, per clip.
24173
+
24174
+ ${c.bold("Quality knobs")}
24175
+ --texture standard|detailed|none \xB7 --geometry detailed \xB7 --quad \xB7 --low-poly
24176
+ --face-limit <n> \xB7 --parts \xB7 --auto-size Each priced in the quote; say it in one line.
24177
+
24178
+ ${c.bold("Common")}
24179
+ --out-dir <dir> \xB7 --no-wait (then genex wait <id>) \xB7 --no-download \xB7 --json
24180
+ A humanoid \`genex model\` result is the input to \`genex character import\` \u2014 the route to a
24181
+ rigged body when the character lane's pose QA refuses a mesh.
24182
+ `,
23952
24183
  budget: `${c.bold("genex budget")} \u2014 this build's asset allowance, and the live numbers to plan against.
23953
24184
 
23954
24185
  ${c.bold("Usage")}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "1.33.0-dev.690",
3
+ "version": "1.34.0-dev.692",
4
4
  "description": "Set up your project's agent workspace (.claude/.codex/.cursor in the game folder), authorize, create a game project, generate AI assets, and publish (genex CLI).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,7 +41,7 @@ Pick per role and say so in one line:
41
41
  distance, 20000-30000 for a third-person player body, 50000+ only for a
42
42
  close-up hero. Moves no cost.
43
43
  - `--texture 2k|4k|8k` (preview / one shot): 8k is +5 credits, for close-ups.
44
- - `--no-ultra` (preview / one shot): −5 credits, less surface detail — stand-ins
44
+ - `--no-ultra` (preview / one shot): −6 credits, less surface detail — stand-ins
45
45
  and crowd enemies.
46
46
  - `--pose a-pose|t-pose` (one shot): the preferred rest pose.
47
47
  - `--height <metres>`: 0.5-3, default 1.7.
@@ -72,6 +72,13 @@ Uthana-rigged body: verbs, `--locomotion` and `--video` work; the Meshy
72
72
  catalog and controller pack do not. Non-biped bodies: `npx genex model
73
73
  import` + `npx genex model rig`.
74
74
 
75
+ The same door takes a `npx genex model` result: a humanoid mesh from the
76
+ model lane, imported here, is a rigged character without the character
77
+ lane's pose QA. If `npx genex character` reports that Meshy's rig failed
78
+ pose QA in both rest poses, the platform already did this for you — the
79
+ character lands rigged by Uthana, with no controller pack, and the terminal
80
+ says so. Animate it exactly like an import.
81
+
75
82
  ## Animating it
76
83
 
77
84
  Say what the character should DO, in plain words — one clip per verb:
@@ -84,15 +91,34 @@ npx genex character motions <character-id> # what is inst
84
91
  ```
85
92
 
86
93
  - `--locomotion` — the full 8-direction walk and run set, 16 clips.
94
+ - A movement request by name: `"walk forward"` is ONE clip in the `walk.forward`
95
+ slot, `"run"` alone is the 8-direction run set, `"strafe"` is walk left + right.
96
+ A boss that only walks at the player needs `"walk forward"`, not the set.
87
97
  - `--video <file>` — use your own footage as the reference.
88
- - `--duration 3-6` — a longer reference for a multi-beat move, so it lands instead of rushing. Bills per second.
98
+ - `--duration 5-8` — a longer reference video for a multi-beat move, so it lands instead of rushing. Bills per second.
89
99
  - `--action <id|query>` — pick a ready-made clip from the library instead of generating.
90
100
 
91
101
  `npx genex creature animate <id> "<verb>"` is the same lane for an enemy.
92
102
 
93
- Free and spend-free: the command prints its PLAN before anything is charged.
103
+ Free and spend-free: the command prints its PLAN before anything is charged
104
+ one line per request naming its route and clip count. READ IT: a phrase that
105
+ reads as movement expands to a set, and a stance or an attack is one clip.
94
106
  Show that plan to the user when the cost matters.
95
107
 
108
+ Derive the list from the game's states first — idle, the moves it actually
109
+ walks, each attack, hurt, defeat — and request those. A full set nobody's
110
+ controller plays is generation time and load time spent on nothing.
111
+
112
+ ## When a batch does not fully land
113
+
114
+ A batch is not all-or-nothing. Clips that landed are installed and paid for;
115
+ the terminal says `N of M clips landed — missing: …` with the exact re-run
116
+ command for the missing verbs, and the credits for what did not land come
117
+ back. `--json` carries `partial: true` and the `failures` list; `wait --all`
118
+ marks the row `partial`. A verb the video model would not film is described
119
+ instead, and the batch names it — watch that clip on the character before
120
+ calling it done.
121
+
96
122
  ## The ready-made library
97
123
 
98
124
  ```bash
@@ -106,20 +132,24 @@ look here first — a library clip costs nothing to generate.
106
132
 
107
133
  Typical: **32** concept · **41** preview · **29** finalize · **64** one-shot
108
134
  character · **46** creature · **18** import (Uthana auto-rig; the upload is
109
- free) · **46 per clip** for a generated move · **free** for a library search
110
- (1 credit = $0.01). `--texture 8k` adds 6, `--no-ultra`
135
+ free) · **46 per clip** for a described move, less for a movement clip and a
136
+ little more for one filmed first · **free** for a library search
137
+ (1 credit = $0.01). `--texture 8k` adds 5, `--no-ultra`
111
138
  takes 6 off. Live prices and your balance: `npx genex doctor`.
112
139
 
113
140
  ## Waiting
114
141
 
115
- Character stages take SEVERAL MINUTES server-side. Do not sit in a foreground
116
- wait: pass `--no-wait`, keep building, then `npx genex wait --all` for one
117
- status line each and `npx genex wait <id>` to pick one up. **Re-running a
118
- character command bills a NEW character.**
142
+ Character stages take SEVERAL MINUTES server-side, and so does a motion
143
+ batch each clip is its own provider job; a 16-clip set is ten minutes or
144
+ more. Do not sit in a foreground wait: pass `--no-wait`, keep building, then
145
+ `npx genex wait --all` for one status line each and `npx genex wait <id>` to
146
+ pick one up. **Re-running a character command bills a NEW character.**
119
147
 
120
148
  ## Troubleshooting
121
149
 
122
150
  - **A four-legged prompt came back upright** — the rig here is biped-shaped only. Generate the body with `npx genex model` and rig it with `npx genex model rig --type quadruped` (see `$genex-tool-model`), or ship it static and animate in code.
151
+ - **"Rigged by Uthana instead" on a `character` / `creature` result** — Meshy's rig failed pose QA in both rest poses (armored and heavily clothed bodies do), so the platform auto-rigged the mesh. Nothing to redo: give it moves with `npx genex character animate <id> --locomotion` and plain-word verbs; the Meshy catalog does not apply.
152
+ - **`character animate` says the character is still generating** — wait for it first (`npx genex wait <id>`); the animate door only opens on a finished rig.
123
153
  - **Rigs rest facing +Z.** Set yaw explicitly when you place one, and never mirror a skinned mesh with a negative scale — it inverts the whole thing silently.
124
154
  - **Out of credits** — the error prints balance, price and refill date. Relay it; don't retry.
125
155
  - **Anything else** — `npx genex doctor`.
@@ -60,10 +60,11 @@ credits get wasted.
60
60
 
61
61
  ## Credits
62
62
 
63
- One balance across everything. Typical prices: texture 3 · sfx 5 · image 8 ·
64
- voice 10 · rig 20 · model 25 · music 40 · video 20 · character 50 · a generated
65
- animation clip 26. **Live prices: `npx genex doctor`** they are set
66
- server-side and these are only typical.
63
+ One balance across everything, and ONE source for what things cost:
64
+ **`npx genex doctor`** (or `npx genex budget`) prints the live price per lane
65
+ and your balance. Prices are set server-side and change without a release, so
66
+ no card types them — the numbers in `npx genex --help` are typical defaults,
67
+ and a quote printed by a command is the exact charge.
67
68
 
68
69
  - A FAILED generation is refunded automatically. You never pay for something that did not arrive.
69
70
  - Out of credits is a plain refusal that prints your balance, the price, and the date credits refill. Relay those facts to the user and carry on with something else — do not retry.