@genex-ai/cli-demo 0.63.0-dev.152 → 0.64.0-dev.154
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 +28 -10
- package/dist/index.js +635 -78
- package/package.json +1 -1
- package/templates/controllers/character/meshy/meshy-loader.ts +9 -0
- package/templates/skills/genex-ai-character/SKILL.md +72 -20
- package/templates/skills/genex-threejs-character-controller/SKILL.md +36 -11
- package/templates/skills/genex-threejs-character-controller/references/animations.md +15 -4
- package/templates/skills/genex-threejs-skill-router/SKILL.md +26 -6
- package/templates/skills/genex-threejs-skill-router/references/routing-map.md +13 -1
- package/templates/skills/genex-threejs-visual-validation/SKILL.md +32 -1
- package/templates/skills/genex-threejs-visual-validation/references/visual-validation.md +21 -0
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
|
|
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) =>
|
|
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
|
-
|
|
546
|
+
fail3(
|
|
547
547
|
new Error(
|
|
548
548
|
"Timed out waiting for authorization. Re-run the command to try again."
|
|
549
549
|
)
|
|
@@ -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
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
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
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
12777
|
-
|
|
12778
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12787
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12811
|
-
|
|
12812
|
-
|
|
12813
|
-
|
|
12814
|
-
|
|
12815
|
-
|
|
12816
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12872
|
-
|
|
12873
|
-
|
|
12874
|
-
|
|
12875
|
-
|
|
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
|
|
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())
|
|
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)
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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))
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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 "<
|
|
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
|
-
--
|
|
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
|
|
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
|
|
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
|
}
|