@genex-ai/cli-demo 0.63.0-dev.152 → 0.65.0-dev.157

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
@@ -495,7 +495,7 @@ async function authorize(authBaseUrl, options) {
495
495
  cleanup();
496
496
  resolve(token);
497
497
  };
498
- const fail2 = (err) => {
498
+ const fail3 = (err) => {
499
499
  if (settled) return;
500
500
  settled = true;
501
501
  cleanup();
@@ -512,7 +512,7 @@ async function authorize(authBaseUrl, options) {
512
512
  }
513
513
  });
514
514
  });
515
- server.on("error", (err) => fail2(err));
515
+ server.on("error", (err) => fail3(err));
516
516
  server.listen(0, "127.0.0.1", () => {
517
517
  const { port } = server.address();
518
518
  const redirectUri = `http://127.0.0.1:${port}/callback`;
@@ -543,7 +543,7 @@ async function authorize(authBaseUrl, options) {
543
543
  );
544
544
  }
545
545
  timer = setTimeout(() => {
546
- fail2(
546
+ fail3(
547
547
  new Error(
548
548
  "Timed out waiting for authorization. Re-run the command to try again."
549
549
  )
@@ -1089,13 +1089,13 @@ function renderGenexConfig() {
1089
1089
  return `// src/genex.config.ts \u2014 written by \`genex init\`. DO NOT hardcode URLs here.
1090
1090
  //
1091
1091
  // One build runs on BOTH stands: the serving hostname picks the stack at runtime
1092
- // (\`-dev.genex.technology\` -> dev), so promoting a game is a copy, never a rebuild.
1092
+ // (\`.auras.cc\` -> dev, else prod), so promoting a game is a copy, never a rebuild.
1093
1093
  // Vite env still overrides, but is loaded ONLY by \`npm run dev\`:
1094
1094
  // .env -> VITE_GENEX_SLUG (this game's identity; committed)
1095
1095
  // .env.development.local -> local-stack URL overrides (dev mode ONLY; gitignored)
1096
1096
  const IS_DEV =
1097
1097
  typeof location !== "undefined" &&
1098
- location.hostname.endsWith("-dev.genex.technology");
1098
+ location.hostname.endsWith(".auras.cc");
1099
1099
  export const GENEX = {
1100
1100
  slug: import.meta.env.VITE_GENEX_SLUG as string,
1101
1101
  apiUrl:
@@ -2585,7 +2585,7 @@ function buildGenOptions(kind, opts) {
2585
2585
  return options;
2586
2586
  }
2587
2587
  async function runGenerate(kind, opts) {
2588
- const log = createLogger({ quiet: opts.quiet });
2588
+ const log = createLogger({ quiet: opts.quiet || opts.json });
2589
2589
  const prompt = opts.prompt?.trim();
2590
2590
  if (!prompt) {
2591
2591
  log.error(`Missing prompt. Usage: ${c.cyan(`genex ${kind} "<prompt>"`)}`);
@@ -2641,31 +2641,52 @@ async function runGenerate(kind, opts) {
2641
2641
  }
2642
2642
  await recordQueued(id, kind, prompt);
2643
2643
  if (opts.noWait) {
2644
- log.success(`Queued (${id}).`);
2645
- log.plain(` Pick it up any time with: ${c.cyan(`npx genex wait ${id}`)}`);
2646
- log.dim(" (Re-running the generate command would start \u2014 and bill \u2014 a NEW generation.)");
2644
+ if (opts.json) {
2645
+ writeJson({
2646
+ kind,
2647
+ id,
2648
+ status: "pending",
2649
+ nextCommand: `genex wait ${id} --json`
2650
+ });
2651
+ } else {
2652
+ log.success(`Queued (${id}).`);
2653
+ log.plain(` Pick it up any time with: ${c.cyan(`npx genex wait ${id}`)}`);
2654
+ log.dim(" (Re-running the generate command would start \u2014 and bill \u2014 a NEW generation.)");
2655
+ }
2647
2656
  return id;
2648
2657
  }
2649
- await awaitAndReport(apiUrl, token, id, kind, log, opts.open);
2658
+ await awaitAndReport(apiUrl, token, id, kind, log, opts.open, opts.json);
2650
2659
  return id;
2651
2660
  }
2652
- async function awaitAndReport(apiUrl, token, id, kind, log, open = false) {
2653
- log.step("Generating\u2026 (this can take up to a minute)");
2654
- const onProgress = (p) => log.dim(` ${p}%`);
2655
- const timeoutMs = waitTimeoutFor(kind);
2656
- const deadline = Date.now() + timeoutMs;
2657
- const streamed = await waitViaSSE(apiUrl, token, id, onProgress, deadline);
2658
- const view = streamed === "unsupported" ? await poll(apiUrl, token, id, onProgress, timeoutMs) : streamed;
2661
+ async function awaitAndReport(apiUrl, token, id, kind, log, open = false, json = false) {
2662
+ if (!json) log.step("Generating\u2026 (this can take up to a minute)");
2663
+ const view = await waitForTerminalView(
2664
+ apiUrl,
2665
+ token,
2666
+ id,
2667
+ kind,
2668
+ json ? () => {
2669
+ } : (p) => log.dim(` ${p}%`)
2670
+ );
2659
2671
  if (!view) {
2660
- log.error("Timed out waiting for the generation.");
2672
+ if (json) writeJson({ kind, id, status: "failed", error: "Timed out waiting for the generation." });
2673
+ else log.error("Timed out waiting for the generation.");
2661
2674
  process.exitCode = 1;
2662
2675
  return;
2663
2676
  }
2664
- await reportTerminal(kind, view, log, open);
2677
+ await reportTerminal(kind, view, log, open, json);
2665
2678
  }
2666
- async function reportTerminal(kind, view, log, open = false) {
2679
+ async function waitForTerminalView(apiUrl, token, id, kind, onProgress = () => {
2680
+ }) {
2681
+ const timeoutMs = waitTimeoutFor(kind);
2682
+ const deadline = Date.now() + timeoutMs;
2683
+ const streamed = await waitViaSSE(apiUrl, token, id, onProgress, deadline);
2684
+ return streamed === "unsupported" ? poll(apiUrl, token, id, onProgress, timeoutMs) : streamed;
2685
+ }
2686
+ async function reportTerminal(kind, view, log, open = false, json = false) {
2667
2687
  if (view.status !== "completed") {
2668
- log.error(`Generation ${view.status}${view.error ? `: ${view.error}` : ""}.`);
2688
+ if (json) writeJson({ kind, id: view.id, status: view.status, error: view.error ?? null });
2689
+ else log.error(`Generation ${view.status}${view.error ? `: ${view.error}` : ""}.`);
2669
2690
  if (view.status === "failed") {
2670
2691
  await recordTerminal(view.id, "failed");
2671
2692
  if (kind === "video") {
@@ -2678,11 +2699,33 @@ async function reportTerminal(kind, view, log, open = false) {
2678
2699
  }
2679
2700
  const files = view.files ?? [];
2680
2701
  if (files.length === 0) {
2681
- log.error("Generation completed but produced no files.");
2702
+ if (json) writeJson({ kind, id: view.id, status: "failed", error: "Generation completed but produced no files." });
2703
+ else log.error("Generation completed but produced no files.");
2682
2704
  process.exitCode = 1;
2683
2705
  return;
2684
2706
  }
2685
2707
  await recordTerminal(view.id, "completed", files.map((f) => f.url));
2708
+ if (kind === "character_concept") {
2709
+ reportCharacterConceptTerminal(view, log, { json, open });
2710
+ return;
2711
+ }
2712
+ if (kind === "character_preview") {
2713
+ reportCharacterPreviewTerminal(view, log, { json, open });
2714
+ return;
2715
+ }
2716
+ if (json) {
2717
+ writeJson({
2718
+ kind,
2719
+ id: view.id,
2720
+ status: view.status,
2721
+ files,
2722
+ ...kind === "character" ? {
2723
+ characterId: view.id,
2724
+ nextCommand: `genex controller character --character ${view.id}`
2725
+ } : {}
2726
+ });
2727
+ return;
2728
+ }
2686
2729
  log.plain("");
2687
2730
  log.success("Done \u2014 live on R2 (nothing downloaded or committed):");
2688
2731
  for (const f of files) {
@@ -2712,6 +2755,8 @@ var WAIT_TIMEOUT_MS = 10 * 60 * 1e3;
2712
2755
  var KIND_WAIT_TIMEOUT_MS = {
2713
2756
  video: 15 * 60 * 1e3,
2714
2757
  // 15 min
2758
+ character_concept: 15 * 60 * 1e3,
2759
+ character_preview: 30 * 60 * 1e3,
2715
2760
  character: 30 * 60 * 1e3,
2716
2761
  character_animation: 20 * 60 * 1e3
2717
2762
  };
@@ -2823,17 +2868,179 @@ function printHint(kind, view, files, log) {
2823
2868
  texture: `Load each map with TextureLoader (RepeatWrapping) into a MeshStandardMaterial \u2014 see genex-ai-texture. Use the URLs above by role.`,
2824
2869
  image: `Load with TextureLoader (set colorSpace = SRGBColorSpace) onto any mesh/plane/sprite \u2014 see genex-ai-image. url = "${url}"`,
2825
2870
  video: `Wire an HTMLVideoElement (crossOrigin="anonymous", muted, loop, playsInline) into a THREE.VideoTexture \u2014 see genex-ai-video. url = "${url}"`,
2871
+ character_concept: "Show all three concept candidates to the user and wait for an explicit selection.",
2872
+ character_preview: "Show all four preview views to the user and wait for explicit approval before finalizing.",
2826
2873
  character: `Install the controller and current manifest with genex controller character --character ${view.id}.`,
2827
2874
  character_animation: "The action is now part of the character's current manifest; refresh the local controller manifest before testing."
2828
2875
  };
2829
2876
  log.dim(` ${hint[kind]}`);
2830
2877
  log.dim(" Reference the URL directly in your code \u2014 don't download it into the repo.");
2831
2878
  }
2879
+ var CONCEPT_STOP = "STOP \u2014 show all three concept images to the user and wait for an explicit selection. Do not create the 3D preview until they choose candidate 1, 2, or 3.";
2880
+ function metadataOf(view) {
2881
+ return view.metadata && typeof view.metadata === "object" ? view.metadata : {};
2882
+ }
2883
+ function stringValue(value) {
2884
+ return typeof value === "string" && value.length > 0 ? value : void 0;
2885
+ }
2886
+ function finiteNumber(value) {
2887
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2888
+ }
2889
+ function conceptCandidates(view) {
2890
+ const candidates = [];
2891
+ for (const file of view.files ?? []) {
2892
+ const match = /^(?:concept-candidate|character-concept)-([123])(?:\.[a-z0-9]+)?$/i.exec(file.role);
2893
+ if (!match) continue;
2894
+ candidates.push({ candidate: Number(match[1]), role: file.role, url: file.url });
2895
+ }
2896
+ return candidates.sort((a, b) => a.candidate - b.candidate);
2897
+ }
2898
+ function conceptContactSheet(view) {
2899
+ return (view.files ?? []).find(
2900
+ (file) => /^(?:concept-gallery|character-concepts-contact-sheet)(?:\.[a-z0-9]+)?$/i.test(file.role)
2901
+ );
2902
+ }
2903
+ function requestedBuildOf(view) {
2904
+ const metadata = metadataOf(view);
2905
+ const raw = metadata.requestedBuild && typeof metadata.requestedBuild === "object" ? metadata.requestedBuild : {};
2906
+ const actionIds = Array.isArray(raw.actionIds) ? raw.actionIds.filter((id) => typeof id === "number" && Number.isInteger(id)) : [];
2907
+ return {
2908
+ actionIds,
2909
+ controllerPack: raw.controllerPack !== false,
2910
+ ...finiteNumber(raw.heightMeters) === void 0 ? {} : { heightMeters: finiteNumber(raw.heightMeters) }
2911
+ };
2912
+ }
2913
+ function characterFinalizeCommand(previewId, intent) {
2914
+ const args = [
2915
+ "genex",
2916
+ "character",
2917
+ "finalize",
2918
+ previewId,
2919
+ "--user-approved",
2920
+ "--approve-remesh",
2921
+ "10000"
2922
+ ];
2923
+ if (intent.heightMeters !== void 0) args.push("--height", String(intent.heightMeters));
2924
+ for (const actionId of intent.actionIds) args.push("--animation", String(actionId));
2925
+ return args.join(" ");
2926
+ }
2927
+ function reportCharacterConceptTerminal(view, log, opts = {}) {
2928
+ const candidates = conceptCandidates(view);
2929
+ if (candidates.length !== 3 || candidates.some((entry, index) => entry.candidate !== index + 1)) {
2930
+ const error = "Character concept generation completed without exactly candidates 1, 2, and 3.";
2931
+ if (opts.json) writeJson({ kind: "character_concept", id: view.id, status: "failed", error });
2932
+ else log.error(error);
2933
+ process.exitCode = 1;
2934
+ return;
2935
+ }
2936
+ const metadata = metadataOf(view);
2937
+ const contactSheet = conceptContactSheet(view);
2938
+ const nextCommand = `genex character preview ${view.id} --candidate <n> --user-approved`;
2939
+ if (opts.open) openUrl(contactSheet?.url ?? candidates[0].url);
2940
+ if (opts.json) {
2941
+ writeJson({
2942
+ kind: "character_concept",
2943
+ stage: "concept",
2944
+ id: view.id,
2945
+ status: view.status,
2946
+ provider: stringValue(metadata.provider),
2947
+ model: stringValue(metadata.aiModel) ?? stringValue(metadata.providerModel),
2948
+ candidates,
2949
+ ...contactSheet ? { contactSheetUrl: contactSheet.url } : {},
2950
+ ...opts.quote ? { quote: opts.quote } : {},
2951
+ requiresUserApproval: true,
2952
+ nextCommand
2953
+ });
2954
+ return;
2955
+ }
2956
+ log.success("Three character concept candidates are ready:");
2957
+ for (const candidate of candidates) log.plain(` ${candidate.candidate}. ${candidate.url}`);
2958
+ if (contactSheet) log.plain(` Contact sheet: ${contactSheet.url}`);
2959
+ log.plain("");
2960
+ log.plain(` ${c.bold("\u{1F441} Show these to the user")} \u2014 include all three numbered links, not only your favorite.`);
2961
+ log.plain(` ${c.bold(CONCEPT_STOP)}`);
2962
+ log.plain(` After approval: ${c.cyan(nextCommand)}`);
2963
+ log.plain("");
2964
+ }
2965
+ var PREVIEW_DIRECTIONS = ["front", "back", "left", "right"];
2966
+ function previewViews(view) {
2967
+ const metadata = metadataOf(view);
2968
+ const rawViews = metadata.views && typeof metadata.views === "object" ? metadata.views : {};
2969
+ const result = {};
2970
+ for (const direction of PREVIEW_DIRECTIONS) {
2971
+ const fromMetadata = stringValue(rawViews[direction]);
2972
+ const fromFile = (view.files ?? []).find((file) => {
2973
+ const role = file.role.toLowerCase();
2974
+ return file.contentType.startsWith("image/") && role.includes(direction);
2975
+ })?.url;
2976
+ result[direction] = fromMetadata ?? fromFile;
2977
+ }
2978
+ return result;
2979
+ }
2980
+ function previewSource(view) {
2981
+ return (view.files ?? []).find(
2982
+ (file) => file.contentType === "model/gltf-binary" || /(?:source-highpoly|character-preview)(?:\.glb)?$/i.test(file.role)
2983
+ )?.url;
2984
+ }
2985
+ function reportCharacterPreviewTerminal(view, log, opts = {}) {
2986
+ const metadata = metadataOf(view);
2987
+ const views = previewViews(view);
2988
+ const sourceFaceCount = finiteNumber(metadata.sourceFaceCount);
2989
+ const missingViews = PREVIEW_DIRECTIONS.filter((direction) => !views[direction]);
2990
+ if (sourceFaceCount === void 0 || missingViews.length > 0) {
2991
+ const detail = [
2992
+ sourceFaceCount === void 0 ? "source face count" : "",
2993
+ missingViews.length > 0 ? `${missingViews.join(", ")} view${missingViews.length === 1 ? "" : "s"}` : ""
2994
+ ].filter(Boolean).join(" and ");
2995
+ const error = `Character preview completed without ${detail}.`;
2996
+ if (opts.json) writeJson({ kind: "character_preview", id: view.id, status: "failed", error });
2997
+ else log.error(error);
2998
+ process.exitCode = 1;
2999
+ return;
3000
+ }
3001
+ const intent = requestedBuildOf(view);
3002
+ const nextCommand = characterFinalizeCommand(view.id, intent);
3003
+ const sourceUrl = previewSource(view);
3004
+ const stop = `STOP \u2014 show the front, back, left, and right views to the user. The high-detail source has ${sourceFaceCount} faces and remains preserved in R2. Ask them to approve a separate 10,000-face triangle rigging copy.`;
3005
+ if (opts.open) openUrl(views.front);
3006
+ if (opts.json) {
3007
+ writeJson({
3008
+ kind: "character_preview",
3009
+ stage: "preview",
3010
+ id: view.id,
3011
+ status: view.status,
3012
+ provider: stringValue(metadata.provider),
3013
+ model: stringValue(metadata.aiModel) ?? stringValue(metadata.providerModel),
3014
+ poseMode: stringValue(metadata.poseMode),
3015
+ shouldRemesh: metadata.shouldRemesh === true,
3016
+ sourceFaceCount,
3017
+ views,
3018
+ ...sourceUrl ? { sourceUrl } : {},
3019
+ requestedBuild: intent,
3020
+ ...opts.quote ? { quote: opts.quote } : {},
3021
+ requiresUserApproval: true,
3022
+ nextCommand
3023
+ });
3024
+ return;
3025
+ }
3026
+ log.success("The high-detail Meshy character preview is ready:");
3027
+ for (const direction of PREVIEW_DIRECTIONS) log.plain(` ${direction}: ${views[direction]}`);
3028
+ if (sourceUrl) log.plain(` High-detail source: ${sourceUrl}`);
3029
+ log.plain("");
3030
+ log.plain(` ${c.bold("\u{1F441} Show these to the user")} \u2014 include all four views.`);
3031
+ log.plain(` ${c.bold(stop)}`);
3032
+ log.plain(` After approval: ${c.cyan(nextCommand)}`);
3033
+ log.plain("");
3034
+ }
3035
+ function writeJson(value) {
3036
+ process.stdout.write(`${JSON.stringify(value)}
3037
+ `);
3038
+ }
2832
3039
 
2833
3040
  // src/commands/wait.ts
2834
3041
  var TERMINAL2 = /* @__PURE__ */ new Set(["completed", "failed"]);
2835
3042
  async function runWait(opts) {
2836
- const log = createLogger({ quiet: opts.quiet });
3043
+ const log = createLogger({ quiet: opts.quiet || opts.json });
2837
3044
  const id = opts.id?.trim();
2838
3045
  if (!id) {
2839
3046
  log.error(
@@ -2881,10 +3088,10 @@ async function runWait(opts) {
2881
3088
  log.dim(` ${kind} ${id}`);
2882
3089
  log.plain("");
2883
3090
  if (TERMINAL2.has(view.status)) {
2884
- await reportTerminal(kind, view, log, opts.open);
3091
+ await reportTerminal(kind, view, log, opts.open, opts.json);
2885
3092
  return;
2886
3093
  }
2887
- await awaitAndReport(apiUrl, token, id, kind, log, opts.open);
3094
+ await awaitAndReport(apiUrl, token, id, kind, log, opts.open, opts.json);
2888
3095
  }
2889
3096
 
2890
3097
  // src/commands/controller.ts
@@ -12583,13 +12790,29 @@ async function installMeshyCharacterManifest(args) {
12583
12790
  }
12584
12791
  const provenance = manifest.provenance;
12585
12792
  if (provenance?.provider === "meshy" && typeof provenance.aiModel === "string") {
12586
- const apiVersion = typeof provenance.apiVersion === "string" && provenance.apiVersion.length > 0 ? `; API ${provenance.apiVersion}` : "";
12793
+ const recordedApiVersion = typeof provenance.meshyApiVersion === "string" && provenance.meshyApiVersion.length > 0 ? provenance.meshyApiVersion : typeof provenance.apiVersion === "string" && provenance.apiVersion.length > 0 ? provenance.apiVersion : void 0;
12794
+ const apiVersion = recordedApiVersion ? `; API ${recordedApiVersion}` : "";
12587
12795
  args.log.success(`Meshy model: ${provenance.aiModel}${apiVersion}`);
12588
12796
  if ((provenance.poseMode === "a-pose" || provenance.poseMode === "t-pose") && typeof provenance.shouldRemesh === "boolean" && typeof provenance.targetPolycount === "number") {
12589
12797
  args.log.dim(
12590
12798
  ` generation: ${provenance.poseMode}, remesh ${provenance.shouldRemesh ? "on" : "off"}, target ${provenance.targetPolycount} polygons`
12591
12799
  );
12592
12800
  }
12801
+ if (typeof provenance.conceptModel === "string" && provenance.conceptModel.length > 0 && typeof provenance.conceptCandidate === "number" && Number.isInteger(provenance.conceptCandidate)) {
12802
+ args.log.success(`Guided source: ${provenance.conceptModel} candidate ${provenance.conceptCandidate}`);
12803
+ const lineage = [
12804
+ typeof provenance.conceptGenerationId === "string" && provenance.conceptGenerationId.length > 0 ? `concept ${provenance.conceptGenerationId}` : void 0,
12805
+ typeof provenance.previewGenerationId === "string" && provenance.previewGenerationId.length > 0 ? `preview ${provenance.previewGenerationId}` : void 0
12806
+ ].filter((value) => value !== void 0);
12807
+ if (lineage.length > 0) args.log.dim(` lineage: ${lineage.join("; ")}`);
12808
+ }
12809
+ if (typeof provenance.sourceFaceCount === "number" && Number.isFinite(provenance.sourceFaceCount) && typeof provenance.remeshTargetFaceCount === "number" && Number.isFinite(provenance.remeshTargetFaceCount)) {
12810
+ const actual = typeof provenance.remeshActualFaceCount === "number" && Number.isFinite(provenance.remeshActualFaceCount) ? `, actual ${provenance.remeshActualFaceCount}` : "";
12811
+ const topology = provenance.remeshTopology === "triangle" ? " triangle" : "";
12812
+ args.log.dim(
12813
+ ` geometry: ${provenance.sourceFaceCount} source faces -> target ${provenance.remeshTargetFaceCount}${topology} faces${actual}`
12814
+ );
12815
+ }
12593
12816
  } else {
12594
12817
  args.log.warn("Meshy model/version metadata is unavailable in this legacy manifest.");
12595
12818
  }
@@ -12772,32 +12995,181 @@ async function quote(url, token, body) {
12772
12995
  }
12773
12996
  return (await response.json()).quote;
12774
12997
  }
12775
- function showAmbiguity(selector, candidates, log) {
12776
- log.error(`\u201C${selector}\u201D is ambiguous. Choose an action id:`);
12777
- for (const candidate of candidates) {
12778
- log.plain(` ${c.cyan(String(candidate.actionId))} ${candidate.name} ${c.dim(candidate.key)}`);
12998
+ function showAmbiguity(selector, candidates, log, json = false) {
12999
+ if (json) {
13000
+ writeJson({
13001
+ kind: "character",
13002
+ status: "selection_required",
13003
+ selector,
13004
+ candidates: candidates.map((candidate) => ({
13005
+ actionId: candidate.actionId,
13006
+ key: candidate.key,
13007
+ name: candidate.name
13008
+ }))
13009
+ });
13010
+ } else {
13011
+ log.error(`\u201C${selector}\u201D is ambiguous. Choose an action id:`);
13012
+ for (const candidate of candidates) {
13013
+ log.plain(` ${c.cyan(String(candidate.actionId))} ${candidate.name} ${c.dim(candidate.key)}`);
13014
+ }
12779
13015
  }
12780
13016
  process.exitCode = 1;
12781
13017
  }
12782
13018
  async function runCharacter(opts) {
12783
- const log = createLogger({ quiet: opts.quiet });
13019
+ if (opts.directText) {
13020
+ await runDirectTextCharacter(opts);
13021
+ return;
13022
+ }
13023
+ await runCharacterConcept(opts);
13024
+ }
13025
+ function fail(opts, message, kind = "character") {
13026
+ if (opts.json) writeJson({ kind, status: "failed", error: message });
13027
+ else createLogger({ quiet: opts.quiet }).error(message);
13028
+ process.exitCode = 1;
13029
+ }
13030
+ function requestedBuild(opts, actionIds) {
13031
+ return {
13032
+ actionIds,
13033
+ controllerPack: opts.controllerPack !== false,
13034
+ ...opts.height === void 0 ? {} : { heightMeters: opts.height }
13035
+ };
13036
+ }
13037
+ async function postWorkflow(url, token, body) {
13038
+ const response = await apiFetch(url, {
13039
+ method: "POST",
13040
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
13041
+ body: JSON.stringify(body)
13042
+ });
13043
+ if (printedStructuredError(response)) return null;
13044
+ if (!response.ok) {
13045
+ const data2 = await response.json().catch(() => ({}));
13046
+ throw new Error(data2.message ?? data2.error ?? `Character workflow request failed (HTTP ${response.status}).`);
13047
+ }
13048
+ const data = await response.json();
13049
+ if (typeof data.id !== "string" || data.id.length === 0) {
13050
+ throw new Error("Character workflow request returned no generation id.");
13051
+ }
13052
+ return { id: data.id, kind: data.kind, status: data.status, creditsQuoted: data.creditsQuoted };
13053
+ }
13054
+ async function runWorkflow(args) {
13055
+ const log = createLogger({ quiet: args.opts.quiet || args.opts.json });
13056
+ const created = await postWorkflow(`${args.ctx.apiUrl}${args.createPath}`, args.ctx.token, args.body);
13057
+ if (!created) {
13058
+ process.exitCode = 1;
13059
+ return;
13060
+ }
13061
+ await recordQueued(created.id, args.kind, args.prompt);
13062
+ if (args.opts.noWait) {
13063
+ const nextCommand = `genex wait ${created.id}${args.opts.json ? " --json" : ""}`;
13064
+ if (args.opts.json) {
13065
+ writeJson({
13066
+ kind: args.kind,
13067
+ id: created.id,
13068
+ status: created.status ?? "pending",
13069
+ quote: args.quote,
13070
+ requiresUserApproval: args.kind !== "character",
13071
+ nextCommand
13072
+ });
13073
+ } else {
13074
+ log.success(`Queued (${created.id}).`);
13075
+ log.plain(` Next: ${c.cyan(nextCommand)}`);
13076
+ log.dim(" Wait on this id to resume. Repeating the same approval reuses its existing job.");
13077
+ }
13078
+ return;
13079
+ }
13080
+ if (!args.opts.json) log.step("Generating\u2026");
13081
+ const view = await waitForTerminalView(
13082
+ args.ctx.apiUrl,
13083
+ args.ctx.token,
13084
+ created.id,
13085
+ args.kind,
13086
+ args.opts.json ? () => {
13087
+ } : (progress) => log.dim(` ${progress}%`)
13088
+ );
13089
+ if (!view) {
13090
+ fail(args.opts, "Timed out waiting for the character workflow.", args.kind);
13091
+ return;
13092
+ }
13093
+ if (view.status !== "completed") {
13094
+ await reportTerminal(args.kind, view, log, false, args.opts.json);
13095
+ return;
13096
+ }
13097
+ await recordTerminal(view.id, "completed", (view.files ?? []).map((file) => file.url));
13098
+ await args.completed(view);
13099
+ }
13100
+ async function runCharacterConcept(opts) {
13101
+ const log = createLogger({ quiet: opts.quiet || opts.json });
12784
13102
  const prompt = opts.prompt?.trim();
12785
13103
  if (!prompt) {
12786
- log.error(`Missing prompt. Usage: ${c.cyan('genex character "<prompt>"')}`);
12787
- process.exitCode = 1;
13104
+ fail(opts, 'Missing brief. Usage: genex character "<brief>".', "character_concept");
13105
+ return;
13106
+ }
13107
+ if (opts.polycount !== void 0) {
13108
+ fail(opts, "--polycount is only available with --direct-text. The approval flow remeshes only after finalize approval.", "character_concept");
13109
+ return;
13110
+ }
13111
+ if (opts.controllerPack === false) {
13112
+ fail(opts, "--no-controller-pack is only available with --direct-text. The guided Meshy parity flow always installs neutral-v3.", "character_concept");
12788
13113
  return;
12789
13114
  }
12790
13115
  const resolved = resolveActionSelectors(opts.actions ?? []);
12791
13116
  if (!resolved.ok) {
12792
- showAmbiguity(resolved.selector, resolved.candidates, log);
13117
+ showAmbiguity(resolved.selector, resolved.candidates, log, opts.json);
12793
13118
  return;
12794
13119
  }
12795
13120
  const ctx = await context(opts);
12796
13121
  if (!ctx) {
12797
- log.error("Not authorized. Run `genex init` first to sign in.");
13122
+ fail(opts, "Not authorized. Run `genex init` first to sign in.", "character_concept");
13123
+ return;
13124
+ }
13125
+ const build = requestedBuild(opts, resolved.actionIds);
13126
+ const request = { prompt, candidates: 3, requestedBuild: build };
13127
+ const price = await quote(`${ctx.apiUrl}/api/characters/concepts/quote`, ctx.token, request);
13128
+ if (!price) {
12798
13129
  process.exitCode = 1;
12799
13130
  return;
12800
13131
  }
13132
+ if (price.candidateCount !== 3) {
13133
+ fail(opts, `The concept quote returned ${price.candidateCount} candidates; this workflow requires exactly 3.`, "character_concept");
13134
+ return;
13135
+ }
13136
+ if (!opts.json) {
13137
+ log.plain(c.bold("Character concept quote"));
13138
+ log.plain(` ${price.credits} Genex credits \xB7 ${price.candidateCount} candidates \xB7 ${price.providerModel}`);
13139
+ log.plain("");
13140
+ }
13141
+ await runWorkflow({
13142
+ opts,
13143
+ ctx,
13144
+ kind: "character_concept",
13145
+ prompt,
13146
+ createPath: "/api/characters/concepts",
13147
+ body: request,
13148
+ quote: price,
13149
+ completed: (view) => reportCharacterConceptTerminal(view, log, {
13150
+ json: opts.json,
13151
+ open: opts.open,
13152
+ quote: price
13153
+ })
13154
+ });
13155
+ }
13156
+ async function runDirectTextCharacter(opts) {
13157
+ const log = createLogger({ quiet: opts.quiet || opts.json });
13158
+ const prompt = opts.prompt?.trim();
13159
+ if (!prompt) {
13160
+ fail(opts, 'Missing prompt. Usage: genex character "<prompt>" --direct-text.');
13161
+ return;
13162
+ }
13163
+ const resolved = resolveActionSelectors(opts.actions ?? []);
13164
+ if (!resolved.ok) {
13165
+ showAmbiguity(resolved.selector, resolved.candidates, log, opts.json);
13166
+ return;
13167
+ }
13168
+ const ctx = await context(opts);
13169
+ if (!ctx) {
13170
+ fail(opts, "Not authorized. Run `genex init` first to sign in.");
13171
+ return;
13172
+ }
12801
13173
  const controllerPack = opts.controllerPack !== false;
12802
13174
  const price = await quote(`${ctx.apiUrl}/api/characters/quote`, ctx.token, {
12803
13175
  actionIds: resolved.actionIds,
@@ -12807,17 +13179,19 @@ async function runCharacter(opts) {
12807
13179
  process.exitCode = 1;
12808
13180
  return;
12809
13181
  }
12810
- log.plain(c.bold("Character quote"));
12811
- log.plain(` ${price.credits} Genex credits = base ${price.baseCredits} + actions ${price.animationCredits}`);
12812
- if (price.controllerPackKey) {
12813
- const version = price.controllerPackVersion === void 0 ? "" : ` v${price.controllerPackVersion}`;
12814
- const fingerprint = price.controllerPackFingerprint ? ` \xB7 fingerprint ${price.controllerPackFingerprint}` : "";
12815
- log.plain(
12816
- ` controller pack ${c.cyan(`${price.controllerPackKey}${version}`)}${fingerprint} \xB7 provider actions ${(price.controllerActionIds ?? []).join(", ")}`
12817
- );
13182
+ if (!opts.json) {
13183
+ log.plain(c.bold("Direct-text character quote"));
13184
+ log.plain(` ${price.credits} Genex credits = base ${price.baseCredits} + actions ${price.animationCredits}`);
13185
+ if (price.controllerPackKey) {
13186
+ const version = price.controllerPackVersion === void 0 ? "" : ` v${price.controllerPackVersion}`;
13187
+ const fingerprint = price.controllerPackFingerprint ? ` \xB7 fingerprint ${price.controllerPackFingerprint}` : "";
13188
+ log.plain(
13189
+ ` controller pack ${c.cyan(`${price.controllerPackKey}${version}`)}${fingerprint} \xB7 provider actions ${(price.controllerActionIds ?? []).join(", ")}`
13190
+ );
13191
+ }
13192
+ log.dim(` ${price.actionsGenerated} Meshy animation task${price.actionsGenerated === 1 ? "" : "s"} in this request.`);
13193
+ log.plain("");
12818
13194
  }
12819
- log.dim(` ${price.actionsGenerated} Meshy animation task${price.actionsGenerated === 1 ? "" : "s"} in this request.`);
12820
- log.plain("");
12821
13195
  await runGenerate("character", {
12822
13196
  ...opts,
12823
13197
  prompt,
@@ -12827,12 +13201,135 @@ async function runCharacter(opts) {
12827
13201
  actionIds: resolved.actionIds,
12828
13202
  controllerPack,
12829
13203
  ...opts.height === void 0 ? {} : { heightMeters: opts.height },
12830
- ...opts.polycount === void 0 ? {} : { targetPolycount: opts.polycount }
13204
+ targetPolycount: opts.polycount ?? 1e4
13205
+ }
13206
+ });
13207
+ }
13208
+ async function runCharacterPreview(opts) {
13209
+ const log = createLogger({ quiet: opts.quiet || opts.json });
13210
+ const conceptId = opts.conceptId?.trim();
13211
+ if (!conceptId) {
13212
+ fail(opts, "Missing concept id. Usage: genex character preview <concept-id> --candidate <1|2|3> --user-approved.", "character_preview");
13213
+ return;
13214
+ }
13215
+ if (!Number.isInteger(opts.candidate) || ![1, 2, 3].includes(opts.candidate)) {
13216
+ fail(opts, "--candidate must be exactly 1, 2, or 3.", "character_preview");
13217
+ return;
13218
+ }
13219
+ if (opts.userApproved !== true) {
13220
+ fail(opts, "STOP: preview creation requires the user's explicit selection. Re-run with --user-approved only after they choose this candidate.", "character_preview");
13221
+ return;
13222
+ }
13223
+ const ctx = await context(opts);
13224
+ if (!ctx) {
13225
+ fail(opts, "Not authorized. Run `genex init` first to sign in.", "character_preview");
13226
+ return;
13227
+ }
13228
+ const body = { candidateIndex: opts.candidate, userApproved: true };
13229
+ const basePath = `/api/characters/concepts/${encodeURIComponent(conceptId)}/previews`;
13230
+ const price = await quote(`${ctx.apiUrl}${basePath}/quote`, ctx.token, body);
13231
+ if (!price) {
13232
+ process.exitCode = 1;
13233
+ return;
13234
+ }
13235
+ if (!opts.json) {
13236
+ log.plain(c.bold("Character 3D preview quote"));
13237
+ log.plain(` ${price.credits} Genex credits \xB7 candidate ${price.candidateIndex} \xB7 ${price.providerModel} \xB7 no remesh`);
13238
+ log.plain("");
13239
+ }
13240
+ await runWorkflow({
13241
+ opts,
13242
+ ctx,
13243
+ kind: "character_preview",
13244
+ prompt: `Preview candidate ${opts.candidate} from concept ${conceptId}`,
13245
+ createPath: basePath,
13246
+ body,
13247
+ quote: price,
13248
+ completed: (view) => reportCharacterPreviewTerminal(view, log, {
13249
+ json: opts.json,
13250
+ open: opts.open,
13251
+ quote: price
13252
+ })
13253
+ });
13254
+ }
13255
+ async function runCharacterFinalize(opts) {
13256
+ const log = createLogger({ quiet: opts.quiet || opts.json });
13257
+ const previewId = opts.previewId?.trim();
13258
+ if (!previewId) {
13259
+ fail(opts, "Missing preview id. Usage: genex character finalize <preview-id> --user-approved --approve-remesh 10000.");
13260
+ return;
13261
+ }
13262
+ if (opts.userApproved !== true) {
13263
+ fail(opts, "STOP: finalization requires explicit user approval of all four preview views. Re-run with --user-approved only after approval.");
13264
+ return;
13265
+ }
13266
+ if (opts.approveRemesh !== 1e4) {
13267
+ fail(opts, "--approve-remesh must be exactly 10000. No other rigging-copy target is accepted.");
13268
+ return;
13269
+ }
13270
+ if (opts.polycount !== void 0) {
13271
+ fail(opts, "Do not combine --polycount with finalize; --approve-remesh 10000 is the only accepted rigging-copy target.");
13272
+ return;
13273
+ }
13274
+ if (opts.controllerPack === false) {
13275
+ fail(opts, "--no-controller-pack is only available with --direct-text. Guided finalization requires neutral-v3.");
13276
+ return;
13277
+ }
13278
+ const resolved = resolveActionSelectors(opts.actions ?? []);
13279
+ if (!resolved.ok) {
13280
+ showAmbiguity(resolved.selector, resolved.candidates, log, opts.json);
13281
+ return;
13282
+ }
13283
+ const ctx = await context(opts);
13284
+ if (!ctx) {
13285
+ fail(opts, "Not authorized. Run `genex init` first to sign in.");
13286
+ return;
13287
+ }
13288
+ const body = {
13289
+ ...requestedBuild(opts, resolved.actionIds),
13290
+ userApproved: true,
13291
+ approvedRemeshTarget: 1e4
13292
+ };
13293
+ const basePath = `/api/characters/previews/${encodeURIComponent(previewId)}/finalize`;
13294
+ const price = await quote(`${ctx.apiUrl}${basePath}/quote`, ctx.token, body);
13295
+ if (!price) {
13296
+ process.exitCode = 1;
13297
+ return;
13298
+ }
13299
+ if (!opts.json) {
13300
+ log.plain(c.bold("Character finalize quote"));
13301
+ log.plain(` ${price.credits} Genex credits = base ${price.baseCredits} + actions ${price.animationCredits}`);
13302
+ log.plain(" Approved rigging copy: 10,000 faces; the high-detail source remains preserved in R2.");
13303
+ log.plain("");
13304
+ }
13305
+ await runWorkflow({
13306
+ opts,
13307
+ ctx,
13308
+ kind: "character",
13309
+ prompt: `Finalize approved character preview ${previewId}`,
13310
+ createPath: basePath,
13311
+ body,
13312
+ quote: price,
13313
+ completed: async (view) => {
13314
+ if (opts.json) {
13315
+ writeJson({
13316
+ kind: "character",
13317
+ stage: "finalize",
13318
+ id: view.id,
13319
+ characterId: view.id,
13320
+ status: view.status,
13321
+ files: view.files ?? [],
13322
+ quote: price,
13323
+ nextCommand: `genex controller character --character ${view.id}`
13324
+ });
13325
+ return;
13326
+ }
13327
+ await reportTerminal("character", view, log);
12831
13328
  }
12832
13329
  });
12833
13330
  }
12834
13331
  async function runCharacterAnimate(opts) {
12835
- const log = createLogger({ quiet: opts.quiet });
13332
+ const log = createLogger({ quiet: opts.quiet || opts.json });
12836
13333
  const characterId = opts.characterId?.trim();
12837
13334
  if (!characterId) {
12838
13335
  log.error("Missing character id.");
@@ -12841,7 +13338,7 @@ async function runCharacterAnimate(opts) {
12841
13338
  }
12842
13339
  const resolved = resolveActionSelectors(opts.actions ?? []);
12843
13340
  if (!resolved.ok) {
12844
- showAmbiguity(resolved.selector, resolved.candidates, log);
13341
+ showAmbiguity(resolved.selector, resolved.candidates, log, opts.json);
12845
13342
  return;
12846
13343
  }
12847
13344
  if (resolved.actionIds.length === 0) {
@@ -12865,14 +13362,26 @@ async function runCharacterAnimate(opts) {
12865
13362
  return;
12866
13363
  }
12867
13364
  if (price.actionsGenerated === 0) {
12868
- log.success(`All ${price.alreadyInstalled ?? resolved.actionIds.length} requested actions are already installed; no credits charged.`);
13365
+ if (opts.json) {
13366
+ writeJson({
13367
+ kind: "character_animation",
13368
+ status: "completed",
13369
+ characterId,
13370
+ alreadyInstalled: price.alreadyInstalled ?? resolved.actionIds.length,
13371
+ credits: 0
13372
+ });
13373
+ } else {
13374
+ log.success(`All ${price.alreadyInstalled ?? resolved.actionIds.length} requested actions are already installed; no credits charged.`);
13375
+ }
12869
13376
  return;
12870
13377
  }
12871
- log.plain(c.bold("Animation quote"));
12872
- log.plain(
12873
- ` ${price.credits} Genex credits = actions ${price.animationCredits}${price.rerigCredits > 0 ? ` + re-rig ${price.rerigCredits}` : ""}`
12874
- );
12875
- log.plain("");
13378
+ if (!opts.json) {
13379
+ log.plain(c.bold("Animation quote"));
13380
+ log.plain(
13381
+ ` ${price.credits} Genex credits = actions ${price.animationCredits}${price.rerigCredits > 0 ? ` + re-rig ${price.rerigCredits}` : ""}`
13382
+ );
13383
+ log.plain("");
13384
+ }
12876
13385
  await runGenerate("character_animation", {
12877
13386
  ...opts,
12878
13387
  prompt: `Attach Meshy actions ${resolved.actionIds.join(", ")} to character ${characterId}`,
@@ -13215,18 +13724,18 @@ async function runUi(opts) {
13215
13724
  process.exitCode = 1;
13216
13725
  }
13217
13726
  }
13218
- function fail(message, details) {
13727
+ function fail2(message, details) {
13219
13728
  throw new Error(details === void 0 ? message : `${message} ${JSON.stringify(details)}`);
13220
13729
  }
13221
13730
  function requireOpt(value, flag, sub) {
13222
- if (!value?.trim()) fail(`Missing ${flag}. Run \`genex ui ${sub}\` \u2014 see \`genex --help\`.`);
13731
+ if (!value?.trim()) fail2(`Missing ${flag}. Run \`genex ui ${sub}\` \u2014 see \`genex --help\`.`);
13223
13732
  return value.trim();
13224
13733
  }
13225
13734
  async function uiExtract(opts, log) {
13226
13735
  const input = requireOpt(opts.input, "--in", "extract");
13227
13736
  const outDir = requireOpt(opts.outDir, "--out-dir", "extract");
13228
13737
  const names = (opts.names ?? "").split(",").map((s) => s.trim()).filter(Boolean);
13229
- if (names.length === 0) fail("Missing --names (comma-separated element names, reading order).");
13738
+ if (names.length === 0) fail2("Missing --names (comma-separated element names, reading order).");
13230
13739
  const minPixels = opts.minPixels ?? 2e3;
13231
13740
  const padding = opts.padding ?? 8;
13232
13741
  const chroma = opts.chroma ?? 240;
@@ -13452,26 +13961,26 @@ async function uiExtract(opts, log) {
13452
13961
  function parseBox(raw, label) {
13453
13962
  const parts = raw.split(",").map((part) => Number.parseInt(part.trim(), 10));
13454
13963
  if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n))) {
13455
- fail(`Invalid ${label} box \u2014 expected x,y,w,h.`, { raw });
13964
+ fail2(`Invalid ${label} box \u2014 expected x,y,w,h.`, { raw });
13456
13965
  }
13457
13966
  const [x, y, w, h] = parts;
13458
- if (w <= 0 || h <= 0) fail(`Invalid ${label} box dimensions.`, { raw });
13967
+ if (w <= 0 || h <= 0) fail2(`Invalid ${label} box dimensions.`, { raw });
13459
13968
  return { x, y, w, h };
13460
13969
  }
13461
13970
  function parsePairs(raw) {
13462
13971
  return raw.split(";").map((part) => part.trim()).filter(Boolean).map((part) => {
13463
13972
  const pieces = part.split(":");
13464
13973
  if (pieces.length !== 3 && pieces.length !== 4) {
13465
- fail(
13974
+ fail2(
13466
13975
  "Invalid pair \u2014 expected name:cleanX,cleanY,w,h:annX,annY,w,h[:expectedComponents].",
13467
13976
  { pair: part }
13468
13977
  );
13469
13978
  }
13470
13979
  const [name, cleanRaw, annotatedRaw, expectedRaw] = pieces;
13471
- if (!/^[a-z0-9][a-z0-9-]*$/i.test(name)) fail("Invalid pair name.", { name });
13980
+ if (!/^[a-z0-9][a-z0-9-]*$/i.test(name)) fail2("Invalid pair name.", { name });
13472
13981
  const expectedComponents = expectedRaw === void 0 ? null : Number.parseInt(expectedRaw, 10);
13473
13982
  if (expectedRaw !== void 0 && (!Number.isFinite(expectedComponents) || expectedComponents < 1)) {
13474
- fail("Invalid expected component count.", { name, expectedRaw });
13983
+ fail2("Invalid expected component count.", { name, expectedRaw });
13475
13984
  }
13476
13985
  return {
13477
13986
  name,
@@ -13483,7 +13992,7 @@ function parsePairs(raw) {
13483
13992
  }
13484
13993
  function assertInside(box, image, label) {
13485
13994
  if (box.x < 0 || box.y < 0 || box.x + box.w > image.width || box.y + box.h > image.height) {
13486
- fail(`${label} crop is outside the source image.`, {
13995
+ fail2(`${label} crop is outside the source image.`, {
13487
13996
  box,
13488
13997
  image: { w: image.width, h: image.height }
13489
13998
  });
@@ -13510,7 +14019,7 @@ function assertNotFlush(png, label, maxFrac) {
13510
14019
  const edges = edgeOpaqueFraction(png);
13511
14020
  const flush2 = Object.entries(edges).filter(([, frac]) => frac > maxFrac);
13512
14021
  if (flush2.length > 0) {
13513
- fail(
14022
+ fail2(
13514
14023
  `${label}: crop box is slicing the element \u2014 opaque content is flush against the image edge. Widen the crop box (or pass --edge-flush-max 1 for a deliberate full-bleed asset).`,
13515
14024
  {
13516
14025
  flushEdges: flush2.map(([edge, frac]) => ({ edge, opaqueFraction: Number(frac.toFixed(3)) })),
@@ -13660,7 +14169,7 @@ async function uiMasks(opts, log) {
13660
14169
  const results = [];
13661
14170
  for (const pair of pairs) {
13662
14171
  if (pair.cleanBox.w !== pair.annotatedBox.w || pair.cleanBox.h !== pair.annotatedBox.h) {
13663
- fail("Clean and annotated crop boxes must have identical dimensions.", pair);
14172
+ fail2("Clean and annotated crop boxes must have identical dimensions.", pair);
13664
14173
  }
13665
14174
  assertInside(pair.cleanBox, image, `${pair.name} clean`);
13666
14175
  assertInside(pair.annotatedBox, image, `${pair.name} annotated`);
@@ -13671,7 +14180,7 @@ async function uiMasks(opts, log) {
13671
14180
  const annotatedBBox = subjectBBox(annotated);
13672
14181
  const registrationDelta = bboxDelta(cleanBBox, annotatedBBox);
13673
14182
  if (registrationDelta > registrationTolerance) {
13674
- fail("Annotated copy registration differs too much from the clean asset.", {
14183
+ fail2("Annotated copy registration differs too much from the clean asset.", {
13675
14184
  name: pair.name,
13676
14185
  registrationDelta: Number(registrationDelta.toFixed(4)),
13677
14186
  registrationTolerance,
@@ -13681,7 +14190,7 @@ async function uiMasks(opts, log) {
13681
14190
  }
13682
14191
  const converted = convertGreenMask(annotated);
13683
14192
  if (converted.coverage < minCoverage || converted.coverage > maxCoverage) {
13684
- fail("Green mask coverage outside the accepted range.", {
14193
+ fail2("Green mask coverage outside the accepted range.", {
13685
14194
  name: pair.name,
13686
14195
  coverage: converted.coverage,
13687
14196
  minCoverage,
@@ -13690,7 +14199,7 @@ async function uiMasks(opts, log) {
13690
14199
  }
13691
14200
  const components = greenComponents(converted.png, minComponentPixels);
13692
14201
  if (pair.expectedComponents && components.length !== pair.expectedComponents) {
13693
- fail("Green mask component count mismatch.", {
14202
+ fail2("Green mask component count mismatch.", {
13694
14203
  name: pair.name,
13695
14204
  expectedComponents: pair.expectedComponents,
13696
14205
  actualComponents: components.length
@@ -13806,7 +14315,7 @@ async function uiTextColor(opts, log) {
13806
14315
  const minFrac = opts.minFrac ?? 4e-3;
13807
14316
  const image = await loadPng(input);
13808
14317
  if (box.x < 0 || box.y < 0 || box.x + box.w > image.width || box.y + box.h > image.height) {
13809
- fail("Box is outside the source image.", { box, image: { w: image.width, h: image.height } });
14318
+ fail2("Box is outside the source image.", { box, image: { w: image.width, h: image.height } });
13810
14319
  }
13811
14320
  const region = cropPng(image, box);
13812
14321
  if (opts.cropPath) await writePng(opts.cropPath, region);
@@ -13879,11 +14388,11 @@ async function uiTrim(opts, log) {
13879
14388
  const input = requireOpt(opts.input, "--in", "trim");
13880
14389
  const outPath = opts.out ?? input;
13881
14390
  if (/^https?:\/\//i.test(outPath)) {
13882
- fail("--in is a URL \u2014 pass --out <png> to say where the trimmed copy goes.");
14391
+ fail2("--in is a URL \u2014 pass --out <png> to say where the trimmed copy goes.");
13883
14392
  }
13884
14393
  const png = await loadPng(input);
13885
14394
  const bbox = alphaBBoxPx(png);
13886
- if (!bbox) fail("Image is fully transparent \u2014 nothing to trim.", { input });
14395
+ if (!bbox) fail2("Image is fully transparent \u2014 nothing to trim.", { input });
13887
14396
  const cropBox = {
13888
14397
  x: bbox.minX,
13889
14398
  y: bbox.minY,
@@ -13916,7 +14425,7 @@ async function uiSeams(opts, log) {
13916
14425
  const tol = opts.seamTolerance ?? DEFAULT_SEAM_TOLERANCE;
13917
14426
  const png = await loadPng(input);
13918
14427
  if (png.width < 4 || png.height < 4) {
13919
- fail("Image is too small to test for tiling.", { width: png.width, height: png.height });
14428
+ fail2("Image is too small to test for tiling.", { width: png.width, height: png.height });
13920
14429
  }
13921
14430
  const r = measureSeam(png, tol);
13922
14431
  const name = input.split("/").pop() ?? input;
@@ -13928,7 +14437,7 @@ async function uiSeams(opts, log) {
13928
14437
  );
13929
14438
  return;
13930
14439
  }
13931
- fail(
14440
+ fail2(
13932
14441
  `Visible ${r.worstAxis} tiling seam in ${name}: the tile boundary jumps ${r.worstRatio}\xD7 the texture's own detail (want \u2264 ${tol}\xD7). Regenerate it seamless/tileable. Don't hide it behind a hand-picked UV repeat \u2014 that trades a seam for a stretched texture (scale belongs in the UVs: worldUV, genex-ai-texture).`
13933
14442
  );
13934
14443
  }
@@ -13955,7 +14464,13 @@ ${c.bold("Usage")}
13955
14464
  genex texture "<prompt>" [options] Generate a PBR texture into public/assets/textures.
13956
14465
  genex image "<prompt>" [options] Generate an image (PNG); prints a public asset URL.
13957
14466
  genex video "<prompt>" [options] Generate a video (mp4); prints a public asset URL.
13958
- genex character "<prompt>" Generate a controller-ready Meshy character.
14467
+ genex character "<brief>" Generate 3 character concepts for user selection.
14468
+ genex character preview <concept-id>
14469
+ Generate an unremeshed Meshy 6 preview of one
14470
+ explicitly user-approved concept candidate.
14471
+ genex character finalize <preview-id>
14472
+ Finalize an approved preview as a 10,000-face
14473
+ controller-ready Meshy character.
13959
14474
  genex character animate <id> Add Meshy library actions to that character.
13960
14475
  genex animations search "<intent>" Search Meshy's animation library by gameplay intent.
13961
14476
  genex wait <id> Attach to a generation enqueued with --no-wait and
@@ -14060,12 +14575,19 @@ ${c.bold("Options for `list`")}
14060
14575
  --auth-url <url> Override the auth site (used only if sign-in is needed).
14061
14576
 
14062
14577
  ${c.bold("Options for `character` / `animations search`")}
14063
- --animation <id|q> (character) add an action; repeatable. Ambiguous queries print
14578
+ --animation <id|q> (character/finalize) add an action; repeatable. Ambiguous queries print
14064
14579
  ranked action IDs instead of choosing silently.
14065
- --no-controller-pack (character) skip the validated idle/walk/run/jump pack.
14580
+ --candidate <1|2|3> (character preview) selected concept candidate.
14581
+ --user-approved Confirm the user explicitly approved the candidate/preview.
14582
+ --approve-remesh 10000
14583
+ (character finalize) approve the separate 10,000-face rigging copy.
14584
+ --direct-text Compatibility path: skip concept/preview approval and generate
14585
+ directly from text (defaults to a 10,000-face target).
14586
+ --no-controller-pack (character --direct-text only) skip the validated locomotion pack.
14066
14587
  --height <meters> (character) target character height, 0.5-3 meters.
14067
- --polycount <count> (character) target 10000-100000 polygons.
14068
- --no-wait (character/animate) enqueue and return a generation id.
14588
+ --polycount <count> (character --direct-text only) target 10000-100000 polygons.
14589
+ --no-wait (character/preview/finalize/animate) enqueue and return a generation id.
14590
+ --json Emit one machine-readable workflow result object.
14069
14591
  --action <id|query> (character animate) action to add; repeatable.
14070
14592
  --category <name> (animations search) restrict results to a Meshy category.
14071
14593
  --in-place (animations search) return provider-declared InPlace actions only.
@@ -14116,7 +14638,10 @@ ${c.bold("Examples")}
14116
14638
  genex ui extract --in <sheet-url> --out-dir ui/ --names health-bar,minimap,ammo
14117
14639
  genex controller character
14118
14640
  genex animations search "rifle reload" --json
14119
- genex character "stylized sci-fi courier, practical clothing" --animation 466 --no-wait
14641
+ genex character "stylized sci-fi courier, practical clothing" --animation 466
14642
+ genex character preview <concept-id> --candidate 2 --user-approved
14643
+ genex character finalize <preview-id> --user-approved --approve-remesh 10000 --animation 466
14644
+ genex character "stylized sci-fi courier" --direct-text --no-wait
14120
14645
  genex character animate <character-id> --action <action-id>
14121
14646
  genex controller character --character <character-id>
14122
14647
  genex controller anims sword pistol
@@ -14175,6 +14700,8 @@ function parseArgs(argv) {
14175
14700
  "--action",
14176
14701
  "--height",
14177
14702
  "--polycount",
14703
+ "--candidate",
14704
+ "--approve-remesh",
14178
14705
  "--category",
14179
14706
  "--limit"
14180
14707
  ]);
@@ -14206,6 +14733,12 @@ function parseArgs(argv) {
14206
14733
  case "--no-controller-pack":
14207
14734
  parsed.options.controllerPack = false;
14208
14735
  break;
14736
+ case "--user-approved":
14737
+ parsed.options.userApproved = true;
14738
+ break;
14739
+ case "--direct-text":
14740
+ parsed.options.directText = true;
14741
+ break;
14209
14742
  case "--in-place":
14210
14743
  parsed.options.inPlace = true;
14211
14744
  break;
@@ -14267,7 +14800,11 @@ function parseArgs(argv) {
14267
14800
  } else if (parsed.command === "character") {
14268
14801
  if (parsed.options.name === "animate" && !parsed.options.characterId) {
14269
14802
  parsed.options.characterId = arg;
14270
- } else if (parsed.options.name !== "animate") {
14803
+ } else if (parsed.options.name === "preview" && !parsed.options.conceptId) {
14804
+ parsed.options.conceptId = arg;
14805
+ } else if (parsed.options.name === "finalize" && !parsed.options.previewId) {
14806
+ parsed.options.previewId = arg;
14807
+ } else if (!["animate", "preview", "finalize"].includes(parsed.options.name ?? "")) {
14271
14808
  parsed.options.name = `${parsed.options.name} ${arg}`;
14272
14809
  } else {
14273
14810
  parsed.error = `Unexpected argument: ${arg}`;
@@ -14369,6 +14906,22 @@ function applyValueFlag(options, flag, value) {
14369
14906
  options.polycount = n;
14370
14907
  break;
14371
14908
  }
14909
+ case "--candidate": {
14910
+ const n = Number(value);
14911
+ if (!Number.isInteger(n) || n < 1 || n > 3) {
14912
+ throw new Error(`Invalid --candidate value: ${value} (expected 1|2|3)`);
14913
+ }
14914
+ options.candidate = n;
14915
+ break;
14916
+ }
14917
+ case "--approve-remesh": {
14918
+ const n = Number(value);
14919
+ if (!Number.isInteger(n) || n !== 1e4) {
14920
+ throw new Error(`Invalid --approve-remesh value: ${value} (expected exactly 10000)`);
14921
+ }
14922
+ options.approveRemesh = n;
14923
+ break;
14924
+ }
14372
14925
  case "--category":
14373
14926
  options.category = value;
14374
14927
  break;
@@ -14533,6 +15086,10 @@ async function main() {
14533
15086
  case "character":
14534
15087
  if (parsed.options.name === "animate") {
14535
15088
  await runCharacterAnimate(parsed.options);
15089
+ } else if (parsed.options.name === "preview") {
15090
+ await runCharacterPreview(parsed.options);
15091
+ } else if (parsed.options.name === "finalize") {
15092
+ await runCharacterFinalize(parsed.options);
14536
15093
  } else {
14537
15094
  await runCharacter({ ...parsed.options, prompt: parsed.options.name });
14538
15095
  }