@genex-ai/cli-demo 1.19.0-dev.601 → 1.20.0-dev.602
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 +484 -156
- package/package.json +1 -1
- package/templates/controllers/character/meshy/meshy-loader.ts +3 -2
- package/templates/skills/genex-ai-character/SKILL.md +67 -15
- package/templates/skills/genex-ai-model/SKILL.md +32 -0
- package/templates/skills/genex-game-director/SKILL.md +7 -5
- package/templates/skills/genex-threejs-character-controller/SKILL.md +2 -1
- package/templates/skills/genex-threejs-creatures/SKILL.md +8 -1
- package/templates/skills/genex-threejs-visual-validation/SKILL.md +2 -1
- package/templates/skills/genex-tool-character/SKILL.md +36 -5
- package/templates/skills/genex-tool-model/SKILL.md +16 -3
package/dist/index.js
CHANGED
|
@@ -7164,9 +7164,20 @@ function applySkyboxGuard(prompt, raw) {
|
|
|
7164
7164
|
if (guarded.length > SKYBOX_PROMPT_MAX) return { prompt, guarded: false, nouns };
|
|
7165
7165
|
return { prompt: guarded, guarded: true, nouns };
|
|
7166
7166
|
}
|
|
7167
|
+
var MODEL_TEXTURE_TIERS = ["standard", "detailed", "none"];
|
|
7167
7168
|
function buildGenOptions(kind, opts) {
|
|
7168
7169
|
const options = { ...opts.generationOptions };
|
|
7169
7170
|
if (kind === "model" && opts.imageUrl) options.imageUrl = opts.imageUrl;
|
|
7171
|
+
if (kind === "model") {
|
|
7172
|
+
if (opts.texture === "none") options.texture = false;
|
|
7173
|
+
else if (opts.texture !== void 0) options.textureQuality = opts.texture;
|
|
7174
|
+
if (opts.geometry !== void 0) options.geometryQuality = opts.geometry;
|
|
7175
|
+
if (opts.quad) options.quad = true;
|
|
7176
|
+
if (opts.lowPoly) options.smartLowPoly = true;
|
|
7177
|
+
if (opts.parts) options.generateParts = true;
|
|
7178
|
+
if (opts.faceLimit !== void 0) options.faceLimit = opts.faceLimit;
|
|
7179
|
+
if (opts.autoSize) options.autoSize = true;
|
|
7180
|
+
}
|
|
7170
7181
|
if (kind === "texture" && opts.terrain) options.terrain = true;
|
|
7171
7182
|
if ((kind === "sfx" || kind === "music") && opts.duration) options.durationSeconds = opts.duration;
|
|
7172
7183
|
if (kind === "voice") {
|
|
@@ -7217,6 +7228,11 @@ async function runGenerate(kind, opts) {
|
|
|
7217
7228
|
const ref = opts.imageUrl.startsWith("data:") ? "local image" : opts.imageUrl;
|
|
7218
7229
|
typedPrompt = `from image: ${path16.basename(ref).slice(0, 120)}`;
|
|
7219
7230
|
}
|
|
7231
|
+
if (kind === "model" && opts.texture !== void 0 && !MODEL_TEXTURE_TIERS.includes(opts.texture)) {
|
|
7232
|
+
log.error(`--texture ${opts.texture} is a character texture size. \`genex model\` takes a texture TIER: ${MODEL_TEXTURE_TIERS.join("|")} (default detailed).`);
|
|
7233
|
+
process.exitCode = 1;
|
|
7234
|
+
return void 0;
|
|
7235
|
+
}
|
|
7220
7236
|
if (!typedPrompt) {
|
|
7221
7237
|
log.error(`Missing prompt. Usage: ${c.cyan(`genex ${kind} "<prompt>"`)}`);
|
|
7222
7238
|
process.exitCode = 1;
|
|
@@ -7982,7 +7998,111 @@ function writeJson(value) {
|
|
|
7982
7998
|
}
|
|
7983
7999
|
|
|
7984
8000
|
// src/commands/model-sub.ts
|
|
7985
|
-
|
|
8001
|
+
import fs18 from "fs/promises";
|
|
8002
|
+
import path17 from "path";
|
|
8003
|
+
var MODEL_SUBCOMMANDS = ["segment", "rig", "animate", "import"];
|
|
8004
|
+
function apiErrorMessage(data, fallback) {
|
|
8005
|
+
if (typeof data !== "object" || data === null) return fallback;
|
|
8006
|
+
const body = data;
|
|
8007
|
+
if (typeof body.message === "string" && body.message) return body.message;
|
|
8008
|
+
if (typeof body.error === "string" && body.error) return body.error;
|
|
8009
|
+
return fallback;
|
|
8010
|
+
}
|
|
8011
|
+
var MODEL_IMPORT_MAX_BYTES = 64 * 1024 * 1024;
|
|
8012
|
+
var GLB_MAGIC = 1179937895;
|
|
8013
|
+
async function importModelFile(args) {
|
|
8014
|
+
const { log } = args;
|
|
8015
|
+
const filePath = args.filePath.trim();
|
|
8016
|
+
if (!/\.glb$/i.test(filePath)) {
|
|
8017
|
+
log.error(`\`import\` takes a .glb file (binary glTF 2.0). Export ${path17.basename(filePath) || "the model"} as GLB first \u2014 Blender: File \u2192 Export \u2192 glTF 2.0, format "glTF Binary".`);
|
|
8018
|
+
return null;
|
|
8019
|
+
}
|
|
8020
|
+
let bytes;
|
|
8021
|
+
try {
|
|
8022
|
+
bytes = await fs18.readFile(filePath);
|
|
8023
|
+
} catch {
|
|
8024
|
+
log.error(`Couldn't read ${filePath}.`);
|
|
8025
|
+
return null;
|
|
8026
|
+
}
|
|
8027
|
+
if (bytes.byteLength > MODEL_IMPORT_MAX_BYTES) {
|
|
8028
|
+
log.error(`${path17.basename(filePath)} is ${(bytes.byteLength / 1e6).toFixed(1)} MB; imports are capped at ${MODEL_IMPORT_MAX_BYTES / 1024 / 1024} MB. Shrink its textures (they are usually the bulk) and try again.`);
|
|
8029
|
+
return null;
|
|
8030
|
+
}
|
|
8031
|
+
if (bytes.byteLength < 12 || bytes.readUInt32LE(0) !== GLB_MAGIC) {
|
|
8032
|
+
log.error(`${path17.basename(filePath)} is not a GLB (no glTF magic). A .gltf + .bin pair must be exported as one binary .glb.`);
|
|
8033
|
+
return null;
|
|
8034
|
+
}
|
|
8035
|
+
const headers = { "Content-Type": "application/json", Authorization: `Bearer ${args.token}` };
|
|
8036
|
+
const minted = await apiFetch(`${args.apiUrl}/api/generations/import`, {
|
|
8037
|
+
method: "POST",
|
|
8038
|
+
headers,
|
|
8039
|
+
body: JSON.stringify({ filename: path17.basename(filePath), bytes: bytes.byteLength, contentType: "model/gltf-binary" })
|
|
8040
|
+
});
|
|
8041
|
+
if (printedStructuredError(minted)) return null;
|
|
8042
|
+
if (!minted.ok) {
|
|
8043
|
+
const data = await minted.json().catch(() => ({}));
|
|
8044
|
+
log.error(apiErrorMessage(data, `Couldn't start the import (HTTP ${minted.status}).`));
|
|
8045
|
+
return null;
|
|
8046
|
+
}
|
|
8047
|
+
const { id, uploadUrl, url } = await minted.json();
|
|
8048
|
+
log.dim(` uploading ${path17.basename(filePath)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
|
|
8049
|
+
const put = await fetch(uploadUrl, {
|
|
8050
|
+
method: "PUT",
|
|
8051
|
+
headers: { "Content-Type": "model/gltf-binary", "Content-Length": String(bytes.byteLength) },
|
|
8052
|
+
body: new Uint8Array(bytes)
|
|
8053
|
+
});
|
|
8054
|
+
if (!put.ok) {
|
|
8055
|
+
log.error(`Upload failed (HTTP ${put.status}). Nothing was charged \u2014 run the import again.`);
|
|
8056
|
+
return null;
|
|
8057
|
+
}
|
|
8058
|
+
const done = await apiFetch(`${args.apiUrl}/api/generations/import/${encodeURIComponent(id)}/complete`, {
|
|
8059
|
+
method: "POST",
|
|
8060
|
+
headers
|
|
8061
|
+
});
|
|
8062
|
+
if (printedStructuredError(done)) return null;
|
|
8063
|
+
if (!done.ok) {
|
|
8064
|
+
const data = await done.json().catch(() => ({}));
|
|
8065
|
+
log.error(apiErrorMessage(data, `The import was refused (HTTP ${done.status}).`));
|
|
8066
|
+
return null;
|
|
8067
|
+
}
|
|
8068
|
+
const view = await done.json();
|
|
8069
|
+
return { id, url, inspection: view.inspection ?? null };
|
|
8070
|
+
}
|
|
8071
|
+
async function runModelImport(opts) {
|
|
8072
|
+
const log = createLogger({ quiet: opts.quiet || opts.json });
|
|
8073
|
+
const filePath = opts.sourceId?.trim();
|
|
8074
|
+
if (!filePath) {
|
|
8075
|
+
log.error(`Missing file. Usage: ${c.cyan("genex model import <file.glb>")}`);
|
|
8076
|
+
process.exitCode = 1;
|
|
8077
|
+
return;
|
|
8078
|
+
}
|
|
8079
|
+
const token = opts.token ?? await readUserToken(opts.envPath);
|
|
8080
|
+
if (!token) {
|
|
8081
|
+
log.error("Not authorized. Run `genex init` first to sign in.");
|
|
8082
|
+
process.exitCode = 1;
|
|
8083
|
+
return;
|
|
8084
|
+
}
|
|
8085
|
+
const project = await readProject();
|
|
8086
|
+
const apiUrl = getApiUrl(opts.apiUrl ?? project?.apiUrl);
|
|
8087
|
+
const imported = await importModelFile({ apiUrl, token, filePath, log });
|
|
8088
|
+
if (!imported) {
|
|
8089
|
+
process.exitCode = 1;
|
|
8090
|
+
return;
|
|
8091
|
+
}
|
|
8092
|
+
const next = {
|
|
8093
|
+
rig: `genex model rig ${imported.id}`,
|
|
8094
|
+
character: `genex character import ${filePath}`
|
|
8095
|
+
};
|
|
8096
|
+
if (opts.json) {
|
|
8097
|
+
writeJson({ kind: "model", id: imported.id, status: "completed", url: imported.url, inspection: imported.inspection, nextCommands: next });
|
|
8098
|
+
return;
|
|
8099
|
+
}
|
|
8100
|
+
const facts = imported.inspection ? ` \xB7 ${imported.inspection.faceCount.toLocaleString("en-US")} faces${imported.inspection.skinned ? " \xB7 already skinned" : ""}` : "";
|
|
8101
|
+
log.success(`Imported (${imported.id})${facts}`);
|
|
8102
|
+
log.plain(` ${imported.url}`);
|
|
8103
|
+
log.plain(` Rig it (7 body plans): ${c.cyan(next.rig)}`);
|
|
8104
|
+
log.plain(` Or make it a playable humanoid: ${c.cyan(next.character)}`);
|
|
8105
|
+
}
|
|
7986
8106
|
var SEGMENT_GRANULARITIES = ["simple", "balanced", "detailed"];
|
|
7987
8107
|
var MODEL_RIG_TYPES = [
|
|
7988
8108
|
"biped",
|
|
@@ -8271,8 +8391,8 @@ async function toRow(e, v, cwd) {
|
|
|
8271
8391
|
}
|
|
8272
8392
|
|
|
8273
8393
|
// src/commands/controller.ts
|
|
8274
|
-
import
|
|
8275
|
-
import
|
|
8394
|
+
import fs20 from "fs/promises";
|
|
8395
|
+
import path19 from "path";
|
|
8276
8396
|
|
|
8277
8397
|
// ../../packages/meshy-animation-catalog/src/index.ts
|
|
8278
8398
|
import { createHash } from "crypto";
|
|
@@ -17402,9 +17522,9 @@ function searchMeshyAnimations(query, options = {}) {
|
|
|
17402
17522
|
}
|
|
17403
17523
|
|
|
17404
17524
|
// src/lib/anims.ts
|
|
17405
|
-
import
|
|
17406
|
-
import
|
|
17407
|
-
var ANIMS_DEST =
|
|
17525
|
+
import fs19 from "fs/promises";
|
|
17526
|
+
import path18 from "path";
|
|
17527
|
+
var ANIMS_DEST = path18.join("public", "assets", "anims");
|
|
17408
17528
|
var HIDDEN_TAG = "reference";
|
|
17409
17529
|
async function runAnims(opts) {
|
|
17410
17530
|
const log = createLogger({ quiet: opts.quiet });
|
|
@@ -17420,7 +17540,7 @@ async function runAnims(opts) {
|
|
|
17420
17540
|
printCatalog(log, manifest, selectors);
|
|
17421
17541
|
return;
|
|
17422
17542
|
}
|
|
17423
|
-
const controllerMarker =
|
|
17543
|
+
const controllerMarker = path18.join(root, "src", "controllers", "character");
|
|
17424
17544
|
if (!await exists2(controllerMarker)) {
|
|
17425
17545
|
log.error(
|
|
17426
17546
|
`No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
|
|
@@ -17429,11 +17549,11 @@ async function runAnims(opts) {
|
|
|
17429
17549
|
process.exitCode = 1;
|
|
17430
17550
|
return;
|
|
17431
17551
|
}
|
|
17432
|
-
const destDir =
|
|
17433
|
-
const gameManifestPath =
|
|
17552
|
+
const destDir = path18.join(root, ANIMS_DEST);
|
|
17553
|
+
const gameManifestPath = path18.join(destDir, "manifest.json");
|
|
17434
17554
|
if (opts.reset) {
|
|
17435
|
-
await
|
|
17436
|
-
log.step(`Cleared ${c.cyan(ANIMS_DEST +
|
|
17555
|
+
await fs19.rm(destDir, { recursive: true, force: true });
|
|
17556
|
+
log.step(`Cleared ${c.cyan(ANIMS_DEST + path18.sep)} (--reset)`);
|
|
17437
17557
|
}
|
|
17438
17558
|
if (selectors.length === 0) {
|
|
17439
17559
|
const installed = await readGameManifest(gameManifestPath);
|
|
@@ -17471,35 +17591,35 @@ async function runAnims(opts) {
|
|
|
17471
17591
|
}
|
|
17472
17592
|
}
|
|
17473
17593
|
const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
17474
|
-
const cacheDir =
|
|
17594
|
+
const cacheDir = path18.join(
|
|
17475
17595
|
opts.cacheDir ?? getAnimsCacheDir(),
|
|
17476
17596
|
`${manifest.library}-v${manifest.version}`
|
|
17477
17597
|
);
|
|
17478
|
-
await
|
|
17479
|
-
await
|
|
17598
|
+
await fs19.mkdir(cacheDir, { recursive: true });
|
|
17599
|
+
await fs19.mkdir(destDir, { recursive: true });
|
|
17480
17600
|
const base = getAnimsBase(opts.animsBase);
|
|
17481
17601
|
let installedCount = 0;
|
|
17482
17602
|
let presentCount = 0;
|
|
17483
17603
|
let addedBytes = 0;
|
|
17484
17604
|
const failures = [];
|
|
17485
17605
|
for (const entry of wanted) {
|
|
17486
|
-
const dest =
|
|
17606
|
+
const dest = path18.join(destDir, entry.file);
|
|
17487
17607
|
if (await hasSize(dest, entry.bytes)) {
|
|
17488
17608
|
presentCount++;
|
|
17489
17609
|
continue;
|
|
17490
17610
|
}
|
|
17491
17611
|
try {
|
|
17492
|
-
const cached =
|
|
17612
|
+
const cached = path18.join(cacheDir, entry.file);
|
|
17493
17613
|
if (!await hasSize(cached, entry.bytes)) {
|
|
17494
17614
|
const res = await fetch(base + entry.file);
|
|
17495
17615
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
17496
17616
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
17497
|
-
await
|
|
17617
|
+
await fs19.writeFile(cached, buf);
|
|
17498
17618
|
}
|
|
17499
|
-
await
|
|
17619
|
+
await fs19.copyFile(cached, dest);
|
|
17500
17620
|
installedCount++;
|
|
17501
17621
|
addedBytes += entry.bytes;
|
|
17502
|
-
log.dim(` ${
|
|
17622
|
+
log.dim(` ${path18.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
|
|
17503
17623
|
} catch (err) {
|
|
17504
17624
|
failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
|
|
17505
17625
|
}
|
|
@@ -17515,13 +17635,13 @@ async function runAnims(opts) {
|
|
|
17515
17635
|
version: manifest.version,
|
|
17516
17636
|
clips: [...union].sort((a, b) => a.localeCompare(b))
|
|
17517
17637
|
};
|
|
17518
|
-
await
|
|
17638
|
+
await fs19.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
|
|
17519
17639
|
log.plain("");
|
|
17520
17640
|
const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
|
|
17521
17641
|
if (presentCount > 0) parts.push(`${presentCount} already present`);
|
|
17522
17642
|
if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
|
|
17523
17643
|
log.success(
|
|
17524
|
-
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST +
|
|
17644
|
+
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path18.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
|
|
17525
17645
|
);
|
|
17526
17646
|
for (const [selector, entries] of resolved) {
|
|
17527
17647
|
const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
|
|
@@ -17553,8 +17673,8 @@ async function loadManifest(baseOverride) {
|
|
|
17553
17673
|
}
|
|
17554
17674
|
} catch {
|
|
17555
17675
|
}
|
|
17556
|
-
const snapshotPath =
|
|
17557
|
-
const manifest = JSON.parse(await
|
|
17676
|
+
const snapshotPath = path18.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
|
|
17677
|
+
const manifest = JSON.parse(await fs19.readFile(snapshotPath, "utf8"));
|
|
17558
17678
|
return { manifest, source: "snapshot" };
|
|
17559
17679
|
}
|
|
17560
17680
|
function resolveSelectors(manifest, selectors) {
|
|
@@ -17672,21 +17792,21 @@ function printCatalog(log, manifest, selectors) {
|
|
|
17672
17792
|
}
|
|
17673
17793
|
async function readGameManifest(file) {
|
|
17674
17794
|
try {
|
|
17675
|
-
return JSON.parse(await
|
|
17795
|
+
return JSON.parse(await fs19.readFile(file, "utf8"));
|
|
17676
17796
|
} catch {
|
|
17677
17797
|
return null;
|
|
17678
17798
|
}
|
|
17679
17799
|
}
|
|
17680
17800
|
async function hasSize(file, bytes) {
|
|
17681
17801
|
try {
|
|
17682
|
-
return (await
|
|
17802
|
+
return (await fs19.stat(file)).size === bytes;
|
|
17683
17803
|
} catch {
|
|
17684
17804
|
return false;
|
|
17685
17805
|
}
|
|
17686
17806
|
}
|
|
17687
17807
|
async function exists2(p) {
|
|
17688
17808
|
try {
|
|
17689
|
-
await
|
|
17809
|
+
await fs19.access(p);
|
|
17690
17810
|
return true;
|
|
17691
17811
|
} catch {
|
|
17692
17812
|
return false;
|
|
@@ -17886,8 +18006,8 @@ var CONTROLLER_FILE_SETS = {
|
|
|
17886
18006
|
]
|
|
17887
18007
|
}
|
|
17888
18008
|
};
|
|
17889
|
-
var CODE_DEST =
|
|
17890
|
-
var ASSETS_DEST =
|
|
18009
|
+
var CODE_DEST = path19.join("src", "controllers");
|
|
18010
|
+
var ASSETS_DEST = path19.join("public", "assets");
|
|
17891
18011
|
async function runController(opts) {
|
|
17892
18012
|
const log = createLogger({ quiet: opts.quiet });
|
|
17893
18013
|
if (opts.kind?.trim() === "anims") {
|
|
@@ -17904,31 +18024,31 @@ async function runController(opts) {
|
|
|
17904
18024
|
process.exitCode = 1;
|
|
17905
18025
|
return;
|
|
17906
18026
|
}
|
|
17907
|
-
const srcDir =
|
|
18027
|
+
const srcDir = path19.join(getTemplatesDir(), "controllers");
|
|
17908
18028
|
const root = opts.cwd ?? process.cwd();
|
|
17909
18029
|
const set = CONTROLLER_FILE_SETS[kind];
|
|
17910
18030
|
log.plain(c.bold(`genex controller ${kind}`));
|
|
17911
18031
|
log.plain("");
|
|
17912
|
-
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST +
|
|
18032
|
+
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path19.sep)}`);
|
|
17913
18033
|
const plan = [
|
|
17914
|
-
...set.code.map((rel) => ({ from: rel, rel:
|
|
18034
|
+
...set.code.map((rel) => ({ from: rel, rel: path19.join(CODE_DEST, rel) })),
|
|
17915
18035
|
...set.assets.map((rel) => ({
|
|
17916
18036
|
from: rel,
|
|
17917
|
-
rel:
|
|
18037
|
+
rel: path19.join(ASSETS_DEST, path19.basename(rel))
|
|
17918
18038
|
}))
|
|
17919
18039
|
];
|
|
17920
18040
|
let copied = 0;
|
|
17921
18041
|
let skipped = 0;
|
|
17922
18042
|
try {
|
|
17923
18043
|
for (const file of plan) {
|
|
17924
|
-
const dest =
|
|
18044
|
+
const dest = path19.join(root, file.rel);
|
|
17925
18045
|
if (!opts.force && await exists3(dest)) {
|
|
17926
18046
|
skipped++;
|
|
17927
18047
|
log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
|
|
17928
18048
|
continue;
|
|
17929
18049
|
}
|
|
17930
|
-
await
|
|
17931
|
-
await
|
|
18050
|
+
await fs20.mkdir(path19.dirname(dest), { recursive: true });
|
|
18051
|
+
await fs20.copyFile(path19.join(srcDir, file.from), dest);
|
|
17932
18052
|
copied++;
|
|
17933
18053
|
log.dim(` ${file.rel}`);
|
|
17934
18054
|
}
|
|
@@ -17981,7 +18101,7 @@ async function runController(opts) {
|
|
|
17981
18101
|
for (const line of set.sketch) {
|
|
17982
18102
|
log.dim(` ${line}`);
|
|
17983
18103
|
}
|
|
17984
|
-
if (kind === "character" && !await exists3(
|
|
18104
|
+
if (kind === "character" && !await exists3(path19.join(root, ASSETS_DEST, "meshy-character.json"))) {
|
|
17985
18105
|
log.plain("");
|
|
17986
18106
|
log.plain(
|
|
17987
18107
|
` ${stepOffset + 3}. This game has no generated character yet \u2014 ${c.cyan(
|
|
@@ -18009,13 +18129,13 @@ async function installMeshyCharacterManifest(args) {
|
|
|
18009
18129
|
}
|
|
18010
18130
|
const body = await response.json();
|
|
18011
18131
|
const manifest = body.manifest;
|
|
18012
|
-
if (!manifest || manifest.schema !== 1 || manifest.rig !== "meshy-biped" || manifest.characterId !== args.characterId || typeof manifest.model?.url !== "string" || typeof manifest.model.skeletonSignature !== "string" || !Array.isArray(manifest.clips) || !manifest.locomotion?.slots) {
|
|
18132
|
+
if (!manifest || manifest.schema !== 1 || manifest.rig !== "meshy-biped" && manifest.rig !== "uthana-biped" || manifest.characterId !== args.characterId || typeof manifest.model?.url !== "string" || typeof manifest.model.skeletonSignature !== "string" || !Array.isArray(manifest.clips) || !manifest.locomotion?.slots) {
|
|
18013
18133
|
throw new Error("The API returned an invalid Meshy character manifest.");
|
|
18014
18134
|
}
|
|
18015
18135
|
assertCompleteMeshyControllerPack(manifest);
|
|
18016
|
-
const destination =
|
|
18017
|
-
await
|
|
18018
|
-
await
|
|
18136
|
+
const destination = path19.join(args.root, ASSETS_DEST, "meshy-character.json");
|
|
18137
|
+
await fs20.mkdir(path19.dirname(destination), { recursive: true });
|
|
18138
|
+
await fs20.writeFile(
|
|
18019
18139
|
destination,
|
|
18020
18140
|
`${JSON.stringify(manifest, null, 2)}
|
|
18021
18141
|
`
|
|
@@ -18025,6 +18145,13 @@ async function installMeshyCharacterManifest(args) {
|
|
|
18025
18145
|
const pack2 = manifest.controllerPack;
|
|
18026
18146
|
if (typeof pack2?.key === "string" && typeof pack2.version === "number") {
|
|
18027
18147
|
args.log.success(`Meshy controller pack ${pack2.key} v${pack2.version}`);
|
|
18148
|
+
} else if (manifest.rig === "uthana-biped") {
|
|
18149
|
+
const slotCount = Object.keys(manifest.locomotion?.slots ?? {}).length;
|
|
18150
|
+
if (slotCount === 0) {
|
|
18151
|
+
args.log.warn(`Uthana-rigged import with no locomotion yet \u2014 run \`genex character animate ${args.characterId} --locomotion\` or the body will stand still.`);
|
|
18152
|
+
} else {
|
|
18153
|
+
args.log.success(`Uthana-rigged import \xB7 ${slotCount} locomotion slot${slotCount === 1 ? "" : "s"} from generated motion`);
|
|
18154
|
+
}
|
|
18028
18155
|
} else {
|
|
18029
18156
|
args.log.warn("Legacy Meshy manifest \u2014 regenerate the character for the immutable preview-reviewed neutral-v3 locomotion pack.");
|
|
18030
18157
|
}
|
|
@@ -18153,14 +18280,14 @@ function assertCompleteMeshyControllerPack(manifest) {
|
|
|
18153
18280
|
}
|
|
18154
18281
|
async function installFallbackAvatar(args) {
|
|
18155
18282
|
const { root, srcDir, log } = args;
|
|
18156
|
-
const dest =
|
|
18157
|
-
await
|
|
18158
|
-
await
|
|
18283
|
+
const dest = path19.join(root, ASSETS_DEST, "avatar.vrm");
|
|
18284
|
+
await fs20.mkdir(path19.dirname(dest), { recursive: true });
|
|
18285
|
+
await fs20.copyFile(path19.join(srcDir, "assets", "default-avatar.vrm"), dest);
|
|
18159
18286
|
log.dim(" public/assets/avatar.vrm (fallback avatar \u2014 bundled CC0 default)");
|
|
18160
18287
|
}
|
|
18161
18288
|
async function exists3(p) {
|
|
18162
18289
|
try {
|
|
18163
|
-
await
|
|
18290
|
+
await fs20.access(p);
|
|
18164
18291
|
return true;
|
|
18165
18292
|
} catch {
|
|
18166
18293
|
return false;
|
|
@@ -18168,8 +18295,8 @@ async function exists3(p) {
|
|
|
18168
18295
|
}
|
|
18169
18296
|
|
|
18170
18297
|
// src/commands/character.ts
|
|
18171
|
-
import
|
|
18172
|
-
import
|
|
18298
|
+
import fs21 from "fs/promises";
|
|
18299
|
+
import path20 from "path";
|
|
18173
18300
|
function exactAnimation(selector) {
|
|
18174
18301
|
const trimmed = selector.trim();
|
|
18175
18302
|
if (/^-?\d+$/.test(trimmed)) return animationById(Number(trimmed));
|
|
@@ -18201,7 +18328,7 @@ async function context(opts) {
|
|
|
18201
18328
|
return { token, apiUrl: getApiUrl(opts.apiUrl ?? project?.apiUrl) };
|
|
18202
18329
|
}
|
|
18203
18330
|
var CHARACTER_BRIEF_MAX_CHARS = 600;
|
|
18204
|
-
function
|
|
18331
|
+
function apiErrorMessage2(data, fallback) {
|
|
18205
18332
|
if (typeof data !== "object" || data === null) return fallback;
|
|
18206
18333
|
const body = data;
|
|
18207
18334
|
if (typeof body.message === "string" && body.message) return body.message;
|
|
@@ -18225,7 +18352,7 @@ async function quote(url, token, body) {
|
|
|
18225
18352
|
if (printedStructuredError(response)) return null;
|
|
18226
18353
|
if (!response.ok) {
|
|
18227
18354
|
const data = await response.json().catch(() => ({}));
|
|
18228
|
-
throw new Error(
|
|
18355
|
+
throw new Error(apiErrorMessage2(data, `Quote failed (HTTP ${response.status}).`));
|
|
18229
18356
|
}
|
|
18230
18357
|
return (await response.json()).quote;
|
|
18231
18358
|
}
|
|
@@ -18249,7 +18376,7 @@ function showAmbiguity(selector, candidates, log, json = false) {
|
|
|
18249
18376
|
}
|
|
18250
18377
|
process.exitCode = 1;
|
|
18251
18378
|
}
|
|
18252
|
-
var INSTALLED_MANIFEST =
|
|
18379
|
+
var INSTALLED_MANIFEST = path20.join("public", "assets", "meshy-character.json");
|
|
18253
18380
|
async function resolveAdoptTarget(selector) {
|
|
18254
18381
|
const trimmed = selector?.trim();
|
|
18255
18382
|
if (trimmed && !trimmed.endsWith(".json")) {
|
|
@@ -18258,7 +18385,7 @@ async function resolveAdoptTarget(selector) {
|
|
|
18258
18385
|
const file = trimmed ?? INSTALLED_MANIFEST;
|
|
18259
18386
|
let raw;
|
|
18260
18387
|
try {
|
|
18261
|
-
raw = await
|
|
18388
|
+
raw = await fs21.readFile(file, "utf8");
|
|
18262
18389
|
} catch {
|
|
18263
18390
|
return {
|
|
18264
18391
|
ok: false,
|
|
@@ -18334,6 +18461,81 @@ async function runCharacterAdopt(opts) {
|
|
|
18334
18461
|
}
|
|
18335
18462
|
});
|
|
18336
18463
|
}
|
|
18464
|
+
async function runCharacterImport(opts) {
|
|
18465
|
+
const log = createLogger({ quiet: opts.quiet || opts.json });
|
|
18466
|
+
const filePath = opts.importPath?.trim();
|
|
18467
|
+
if (!filePath) {
|
|
18468
|
+
fail2(opts, "Missing file. Usage: genex character import <file.glb> [--height 1.8] [--no-fingers]");
|
|
18469
|
+
return;
|
|
18470
|
+
}
|
|
18471
|
+
if (opts.texture !== void 0 || opts.ultra === false || opts.pose !== void 0 || opts.polycount !== void 0) {
|
|
18472
|
+
fail2(opts, "--texture / --no-ultra / --pose / --polycount are Meshy generation knobs; an import keeps your mesh exactly as it is. Uthana only adds the skeleton (--no-fingers skips finger joints).");
|
|
18473
|
+
return;
|
|
18474
|
+
}
|
|
18475
|
+
if (opts.controllerPack === false || (opts.actions?.length ?? 0) > 0) {
|
|
18476
|
+
fail2(opts, "An imported character has no Meshy controller pack and takes no catalog --animation: its motion comes from `genex character animate <id> --locomotion` and plain-word verbs.");
|
|
18477
|
+
return;
|
|
18478
|
+
}
|
|
18479
|
+
const ctx = await context(opts);
|
|
18480
|
+
if (!ctx) {
|
|
18481
|
+
fail2(opts, "Not authorized. Run `genex init` first to sign in.");
|
|
18482
|
+
return;
|
|
18483
|
+
}
|
|
18484
|
+
const imported = await importModelFile({ apiUrl: ctx.apiUrl, token: ctx.token, filePath, log });
|
|
18485
|
+
if (!imported) {
|
|
18486
|
+
process.exitCode = 1;
|
|
18487
|
+
return;
|
|
18488
|
+
}
|
|
18489
|
+
const body = {
|
|
18490
|
+
sourceGenerationId: imported.id,
|
|
18491
|
+
includeFingers: opts.fingers !== false,
|
|
18492
|
+
...opts.height === void 0 ? {} : { heightMeters: opts.height }
|
|
18493
|
+
};
|
|
18494
|
+
const price = await quote(`${ctx.apiUrl}/api/characters/import/quote`, ctx.token, body);
|
|
18495
|
+
if (!price) {
|
|
18496
|
+
process.exitCode = 1;
|
|
18497
|
+
return;
|
|
18498
|
+
}
|
|
18499
|
+
if (!opts.json) {
|
|
18500
|
+
log.plain(c.bold("Character import quote"));
|
|
18501
|
+
log.plain(` ${price.credits} Genex credits \xB7 Uthana auto-rig${opts.fingers === false ? " (no finger joints)" : " with finger joints"} \xB7 the model upload was free`);
|
|
18502
|
+
log.plain("");
|
|
18503
|
+
}
|
|
18504
|
+
await runWorkflow({
|
|
18505
|
+
opts,
|
|
18506
|
+
ctx,
|
|
18507
|
+
kind: "character",
|
|
18508
|
+
prompt: `Import ${path20.basename(filePath)} as a rigged character`,
|
|
18509
|
+
createPath: "/api/characters/import",
|
|
18510
|
+
body,
|
|
18511
|
+
quote: price,
|
|
18512
|
+
completed: (view) => {
|
|
18513
|
+
const confidence = typeof view.metadata?.autoRigConfidence === "number" ? view.metadata.autoRigConfidence : null;
|
|
18514
|
+
const next = {
|
|
18515
|
+
locomotion: `genex character animate ${view.id} --locomotion`,
|
|
18516
|
+
install: `genex controller character --character ${view.id}`
|
|
18517
|
+
};
|
|
18518
|
+
if (opts.json) {
|
|
18519
|
+
writeJson({
|
|
18520
|
+
kind: "character",
|
|
18521
|
+
status: view.status,
|
|
18522
|
+
characterId: view.id,
|
|
18523
|
+
modelGenerationId: imported.id,
|
|
18524
|
+
rigProvider: "uthana",
|
|
18525
|
+
autoRigConfidence: confidence,
|
|
18526
|
+
nextCommand: next.locomotion,
|
|
18527
|
+
nextCommands: next
|
|
18528
|
+
});
|
|
18529
|
+
return;
|
|
18530
|
+
}
|
|
18531
|
+
log.success(`Rigged by Uthana${confidence === null ? "" : ` (confidence ${confidence.toFixed(2)})`}: ${c.cyan(view.id)}`);
|
|
18532
|
+
log.plain(" It has a skeleton and no clips yet. Give it the walk/run set, then point the game at it:");
|
|
18533
|
+
log.plain(` ${c.cyan(next.locomotion)}`);
|
|
18534
|
+
log.plain(` ${c.cyan(next.install)}`);
|
|
18535
|
+
log.dim(' Any move in plain words: genex character animate <id> "<what it should do>" \xB7 the Meshy catalog does not apply to this rig.');
|
|
18536
|
+
}
|
|
18537
|
+
});
|
|
18538
|
+
}
|
|
18337
18539
|
async function runCharacter(opts) {
|
|
18338
18540
|
if (opts.directText) {
|
|
18339
18541
|
await runDirectTextCharacter(opts);
|
|
@@ -18362,7 +18564,7 @@ async function postWorkflow(url, token, body) {
|
|
|
18362
18564
|
if (printedStructuredError(response)) return null;
|
|
18363
18565
|
if (!response.ok) {
|
|
18364
18566
|
const data2 = await response.json().catch(() => ({}));
|
|
18365
|
-
throw new Error(
|
|
18567
|
+
throw new Error(apiErrorMessage2(data2, `Character workflow request failed (HTTP ${response.status}).`));
|
|
18366
18568
|
}
|
|
18367
18569
|
const data = await response.json();
|
|
18368
18570
|
if (typeof data.id !== "string" || data.id.length === 0) {
|
|
@@ -18432,7 +18634,11 @@ async function runCharacterConcept(opts) {
|
|
|
18432
18634
|
return;
|
|
18433
18635
|
}
|
|
18434
18636
|
if (opts.polycount !== void 0) {
|
|
18435
|
-
fail2(opts, "--polycount is only available with --direct-text. The approval flow
|
|
18637
|
+
fail2(opts, "--polycount is only available with --direct-text. The approval flow takes its face budget on `character finalize --approve-remesh <faces>`.", "character_concept");
|
|
18638
|
+
return;
|
|
18639
|
+
}
|
|
18640
|
+
if (opts.texture !== void 0 || opts.ultra === false || opts.pose !== void 0) {
|
|
18641
|
+
fail2(opts, "--texture / --no-ultra / --pose apply to the paid mesh call: pass them to `character preview` (guided) or `character --direct-text` / `creature` (one shot). Concepts are images.", "character_concept");
|
|
18436
18642
|
return;
|
|
18437
18643
|
}
|
|
18438
18644
|
if (opts.controllerPack === false) {
|
|
@@ -18528,10 +18734,26 @@ async function runDirectTextCharacter(opts) {
|
|
|
18528
18734
|
actionIds: resolved.actionIds,
|
|
18529
18735
|
controllerPack,
|
|
18530
18736
|
...opts.height === void 0 ? {} : { heightMeters: opts.height },
|
|
18531
|
-
targetPolycount: opts.polycount ?? 1e4
|
|
18737
|
+
targetPolycount: opts.polycount ?? 1e4,
|
|
18738
|
+
...meshyQualityOptions(opts),
|
|
18739
|
+
...opts.pose === void 0 ? {} : { poseMode: opts.pose }
|
|
18532
18740
|
}
|
|
18533
18741
|
});
|
|
18534
18742
|
}
|
|
18743
|
+
var CHARACTER_TEXTURE_SIZES = ["2k", "4k", "8k"];
|
|
18744
|
+
var REMESH_TARGET_MIN = 1e4;
|
|
18745
|
+
var REMESH_TARGET_MAX = 1e5;
|
|
18746
|
+
function meshyQualityOptions(opts) {
|
|
18747
|
+
return {
|
|
18748
|
+
...opts.texture === void 0 ? {} : { textureResolution: opts.texture },
|
|
18749
|
+
...opts.ultra === false ? { ultraMode: false } : {}
|
|
18750
|
+
};
|
|
18751
|
+
}
|
|
18752
|
+
function badCharacterTexture(opts) {
|
|
18753
|
+
if (opts.texture === void 0) return null;
|
|
18754
|
+
if (CHARACTER_TEXTURE_SIZES.includes(opts.texture)) return null;
|
|
18755
|
+
return `--texture ${opts.texture} is a \`genex model\` tier. A character takes a texture SIZE: ${CHARACTER_TEXTURE_SIZES.join("|")} (default 4k).`;
|
|
18756
|
+
}
|
|
18535
18757
|
async function runCharacterPreview(opts) {
|
|
18536
18758
|
const log = createLogger({ quiet: opts.quiet || opts.json });
|
|
18537
18759
|
const conceptId = opts.conceptId?.trim();
|
|
@@ -18552,7 +18774,12 @@ async function runCharacterPreview(opts) {
|
|
|
18552
18774
|
fail2(opts, "Not authorized. Run `genex init` first to sign in.", "character_preview");
|
|
18553
18775
|
return;
|
|
18554
18776
|
}
|
|
18555
|
-
const
|
|
18777
|
+
const badTexture = badCharacterTexture(opts);
|
|
18778
|
+
if (badTexture) {
|
|
18779
|
+
fail2(opts, badTexture, "character_preview");
|
|
18780
|
+
return;
|
|
18781
|
+
}
|
|
18782
|
+
const body = { candidateIndex: opts.candidate, userApproved: true, ...meshyQualityOptions(opts) };
|
|
18556
18783
|
const basePath = `/api/characters/concepts/${encodeURIComponent(conceptId)}/previews`;
|
|
18557
18784
|
const price = await quote(`${ctx.apiUrl}${basePath}/quote`, ctx.token, body);
|
|
18558
18785
|
if (!price) {
|
|
@@ -18590,12 +18817,20 @@ async function runCharacterFinalize(opts) {
|
|
|
18590
18817
|
fail2(opts, "STOP: finalization requires explicit user approval of all four preview views. Re-run with --user-approved only after approval.");
|
|
18591
18818
|
return;
|
|
18592
18819
|
}
|
|
18593
|
-
|
|
18594
|
-
|
|
18820
|
+
const remeshTarget = opts.approveRemesh;
|
|
18821
|
+
if (remeshTarget === void 0 || !Number.isInteger(remeshTarget) || remeshTarget < REMESH_TARGET_MIN || remeshTarget > REMESH_TARGET_MAX) {
|
|
18822
|
+
fail2(
|
|
18823
|
+
opts,
|
|
18824
|
+
`--approve-remesh <faces> is required: the face budget of the rigging copy, ${REMESH_TARGET_MIN}-${REMESH_TARGET_MAX} (10000 for a crowd body, 30000 or more for a hero). The number is the approval.`
|
|
18825
|
+
);
|
|
18595
18826
|
return;
|
|
18596
18827
|
}
|
|
18597
18828
|
if (opts.polycount !== void 0) {
|
|
18598
|
-
fail2(opts, "Do not combine --polycount with finalize; --approve-remesh
|
|
18829
|
+
fail2(opts, "Do not combine --polycount with finalize; --approve-remesh <faces> is the rigging-copy target.");
|
|
18830
|
+
return;
|
|
18831
|
+
}
|
|
18832
|
+
if (opts.texture !== void 0 || opts.ultra === false) {
|
|
18833
|
+
fail2(opts, "--texture / --no-ultra belong to `genex character preview` \u2014 the preview is the paid Meshy call; finalize remeshes and rigs what it made.");
|
|
18599
18834
|
return;
|
|
18600
18835
|
}
|
|
18601
18836
|
if (opts.controllerPack === false) {
|
|
@@ -18615,7 +18850,7 @@ async function runCharacterFinalize(opts) {
|
|
|
18615
18850
|
const body = {
|
|
18616
18851
|
...requestedBuild(opts, resolved.actionIds),
|
|
18617
18852
|
userApproved: true,
|
|
18618
|
-
approvedRemeshTarget:
|
|
18853
|
+
approvedRemeshTarget: remeshTarget
|
|
18619
18854
|
};
|
|
18620
18855
|
const basePath = `/api/characters/previews/${encodeURIComponent(previewId)}/finalize`;
|
|
18621
18856
|
const price = await quote(`${ctx.apiUrl}${basePath}/quote`, ctx.token, body);
|
|
@@ -18626,7 +18861,7 @@ async function runCharacterFinalize(opts) {
|
|
|
18626
18861
|
if (!opts.json) {
|
|
18627
18862
|
log.plain(c.bold("Character finalize quote"));
|
|
18628
18863
|
log.plain(` ${price.credits} Genex credits = base ${price.baseCredits} + actions ${price.animationCredits}`);
|
|
18629
|
-
log.plain(
|
|
18864
|
+
log.plain(` Approved rigging copy: ${remeshTarget.toLocaleString("en-US")} faces; the high-detail source remains preserved in R2.`);
|
|
18630
18865
|
log.plain("");
|
|
18631
18866
|
}
|
|
18632
18867
|
await runWorkflow({
|
|
@@ -18741,22 +18976,22 @@ async function context2(opts) {
|
|
|
18741
18976
|
const project = await readProject();
|
|
18742
18977
|
return { token, apiUrl: getApiUrl(opts.apiUrl ?? project?.apiUrl) };
|
|
18743
18978
|
}
|
|
18744
|
-
async function readVideo(
|
|
18979
|
+
async function readVideo(path27, log) {
|
|
18745
18980
|
let bytes;
|
|
18746
18981
|
try {
|
|
18747
|
-
bytes = await readFile(
|
|
18982
|
+
bytes = await readFile(path27);
|
|
18748
18983
|
} catch {
|
|
18749
|
-
log.error(`Can't read ${
|
|
18984
|
+
log.error(`Can't read ${path27}.`);
|
|
18750
18985
|
return null;
|
|
18751
18986
|
}
|
|
18752
18987
|
if (bytes.byteLength > MAX_VIDEO_BYTES) {
|
|
18753
|
-
log.error(`${basename(
|
|
18988
|
+
log.error(`${basename(path27)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
|
|
18754
18989
|
return null;
|
|
18755
18990
|
}
|
|
18756
18991
|
return bytes;
|
|
18757
18992
|
}
|
|
18758
|
-
async function uploadVideo(apiUrl, token, characterId,
|
|
18759
|
-
const contentType = /\.mov$/i.test(
|
|
18993
|
+
async function uploadVideo(apiUrl, token, characterId, path27, bytes, log) {
|
|
18994
|
+
const contentType = /\.mov$/i.test(path27) ? "video/quicktime" : "video/mp4";
|
|
18760
18995
|
const minted = await apiFetch(
|
|
18761
18996
|
`${apiUrl}/api/characters/${encodeURIComponent(characterId)}/motions/video-url`,
|
|
18762
18997
|
{
|
|
@@ -18771,7 +19006,7 @@ async function uploadVideo(apiUrl, token, characterId, path26, bytes, log) {
|
|
|
18771
19006
|
return null;
|
|
18772
19007
|
}
|
|
18773
19008
|
const { uploadUrl, videoUrl } = await minted.json();
|
|
18774
|
-
log.dim(` uploading ${basename(
|
|
19009
|
+
log.dim(` uploading ${basename(path27)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
|
|
18775
19010
|
const put = await fetch(uploadUrl, {
|
|
18776
19011
|
method: "PUT",
|
|
18777
19012
|
headers: { "Content-Type": contentType, "Content-Length": String(bytes.byteLength) },
|
|
@@ -19087,8 +19322,8 @@ function rank(items, query) {
|
|
|
19087
19322
|
}
|
|
19088
19323
|
|
|
19089
19324
|
// src/commands/motion.ts
|
|
19090
|
-
import
|
|
19091
|
-
import
|
|
19325
|
+
import fs22 from "fs/promises";
|
|
19326
|
+
import path21 from "path";
|
|
19092
19327
|
|
|
19093
19328
|
// src/lib/motion/npz.ts
|
|
19094
19329
|
import zlib from "zlib";
|
|
@@ -20339,7 +20574,7 @@ async function motionGen(opts, log) {
|
|
|
20339
20574
|
}
|
|
20340
20575
|
if (opts.constraintsPath !== void 0) {
|
|
20341
20576
|
try {
|
|
20342
|
-
const raw = await
|
|
20577
|
+
const raw = await fs22.readFile(opts.constraintsPath, "utf8");
|
|
20343
20578
|
generationOptions.constraints = JSON.parse(raw);
|
|
20344
20579
|
} catch {
|
|
20345
20580
|
log.error(`Couldn't read the --constraints JSON at ${opts.constraintsPath}.`);
|
|
@@ -20363,10 +20598,10 @@ async function motionGen(opts, log) {
|
|
|
20363
20598
|
async function expandTakes(selectors) {
|
|
20364
20599
|
const out = [];
|
|
20365
20600
|
for (const sel of selectors) {
|
|
20366
|
-
const st = await
|
|
20601
|
+
const st = await fs22.stat(sel).catch(() => null);
|
|
20367
20602
|
if (st?.isDirectory()) {
|
|
20368
|
-
const names = await
|
|
20369
|
-
for (const n of names.sort()) if (n.endsWith(".npz")) out.push(
|
|
20603
|
+
const names = await fs22.readdir(sel);
|
|
20604
|
+
for (const n of names.sort()) if (n.endsWith(".npz")) out.push(path21.join(sel, n));
|
|
20370
20605
|
} else if (st?.isFile()) {
|
|
20371
20606
|
out.push(sel);
|
|
20372
20607
|
} else {
|
|
@@ -20401,7 +20636,7 @@ async function motionVerify(opts, log) {
|
|
|
20401
20636
|
let gates = DEFAULT_GATES;
|
|
20402
20637
|
if (opts.gatesPath) {
|
|
20403
20638
|
try {
|
|
20404
|
-
gates = mergeGates(DEFAULT_GATES, JSON.parse(await
|
|
20639
|
+
gates = mergeGates(DEFAULT_GATES, JSON.parse(await fs22.readFile(opts.gatesPath, "utf8")));
|
|
20405
20640
|
} catch {
|
|
20406
20641
|
log.error(`Couldn't read the --gates JSON at ${opts.gatesPath}.`);
|
|
20407
20642
|
process.exitCode = 1;
|
|
@@ -20423,9 +20658,9 @@ async function motionVerify(opts, log) {
|
|
|
20423
20658
|
}
|
|
20424
20659
|
const reports = [];
|
|
20425
20660
|
for (const file of files) {
|
|
20426
|
-
const stem =
|
|
20661
|
+
const stem = path21.basename(file).replace(/\.npz$/, "");
|
|
20427
20662
|
try {
|
|
20428
|
-
reports.push(analyzeTake(stem, await
|
|
20663
|
+
reports.push(analyzeTake(stem, await fs22.readFile(file), gates));
|
|
20429
20664
|
} catch (err) {
|
|
20430
20665
|
reports.push({
|
|
20431
20666
|
take: stem,
|
|
@@ -20463,7 +20698,7 @@ async function motionCompile(opts, log) {
|
|
|
20463
20698
|
let cfg = DEFAULT_MOTION_CONFIG;
|
|
20464
20699
|
if (opts.configPath) {
|
|
20465
20700
|
try {
|
|
20466
|
-
const patch = JSON.parse(await
|
|
20701
|
+
const patch = JSON.parse(await fs22.readFile(opts.configPath, "utf8"));
|
|
20467
20702
|
cfg = { ...DEFAULT_MOTION_CONFIG, ...patch, idleLoop: { ...DEFAULT_MOTION_CONFIG.idleLoop, ...patch.idleLoop } };
|
|
20468
20703
|
} catch {
|
|
20469
20704
|
log.error(`Couldn't read the --config JSON at ${opts.configPath}.`);
|
|
@@ -20481,16 +20716,16 @@ async function motionCompile(opts, log) {
|
|
|
20481
20716
|
}
|
|
20482
20717
|
const inputs = [];
|
|
20483
20718
|
for (const file of files) {
|
|
20484
|
-
const stem =
|
|
20719
|
+
const stem = path21.basename(file).replace(/\.npz$/, "");
|
|
20485
20720
|
try {
|
|
20486
|
-
inputs.push({ stem, take: loadTake(await
|
|
20721
|
+
inputs.push({ stem, take: loadTake(await fs22.readFile(file)) });
|
|
20487
20722
|
} catch (err) {
|
|
20488
20723
|
log.error(`${stem}: ${err instanceof Error ? err.message : String(err)}`);
|
|
20489
20724
|
process.exitCode = 1;
|
|
20490
20725
|
return;
|
|
20491
20726
|
}
|
|
20492
20727
|
}
|
|
20493
|
-
const setName = opts.set ??
|
|
20728
|
+
const setName = opts.set ?? path21.basename(opts.out).replace(/\.json$/, "");
|
|
20494
20729
|
let result;
|
|
20495
20730
|
try {
|
|
20496
20731
|
result = compileSet(inputs, setName, cfg);
|
|
@@ -20505,9 +20740,9 @@ async function motionCompile(opts, log) {
|
|
|
20505
20740
|
process.exitCode = 1;
|
|
20506
20741
|
return;
|
|
20507
20742
|
}
|
|
20508
|
-
await
|
|
20743
|
+
await fs22.mkdir(path21.dirname(path21.resolve(opts.out)), { recursive: true });
|
|
20509
20744
|
const json = JSON.stringify(result.data);
|
|
20510
|
-
await
|
|
20745
|
+
await fs22.writeFile(opts.out, json);
|
|
20511
20746
|
if (opts.json) {
|
|
20512
20747
|
writeJson({ out: opts.out, set: setName, tracks: Object.keys(result.data.gaits), bytes: json.length });
|
|
20513
20748
|
return;
|
|
@@ -20525,9 +20760,9 @@ var MOTION_RUNTIME_FILES = [
|
|
|
20525
20760
|
var MOTION_PRESETS = {
|
|
20526
20761
|
rifle: ["sets/rifle.json", "sets/jumps.json"]
|
|
20527
20762
|
};
|
|
20528
|
-
var MOTION_DEST =
|
|
20763
|
+
var MOTION_DEST = path21.join("src", "motion");
|
|
20529
20764
|
async function motionInstall(opts, log) {
|
|
20530
|
-
const srcDir =
|
|
20765
|
+
const srcDir = path21.join(getTemplatesDir(), "motion");
|
|
20531
20766
|
const root = opts.cwd ?? process.cwd();
|
|
20532
20767
|
const preset = opts.set;
|
|
20533
20768
|
if (preset !== void 0 && !MOTION_PRESETS[preset]) {
|
|
@@ -20538,21 +20773,21 @@ async function motionInstall(opts, log) {
|
|
|
20538
20773
|
const files = [...MOTION_RUNTIME_FILES, ...preset ? MOTION_PRESETS[preset] : []];
|
|
20539
20774
|
log.plain(c.bold(`genex motion install${preset ? ` --set ${preset}` : ""}`));
|
|
20540
20775
|
log.plain("");
|
|
20541
|
-
log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST +
|
|
20776
|
+
log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST + path21.sep)}`);
|
|
20542
20777
|
let copied = 0, skipped = 0;
|
|
20543
20778
|
try {
|
|
20544
20779
|
for (const rel of files) {
|
|
20545
|
-
const dest =
|
|
20546
|
-
const exists5 = await
|
|
20780
|
+
const dest = path21.join(root, MOTION_DEST, rel);
|
|
20781
|
+
const exists5 = await fs22.access(dest).then(() => true, () => false);
|
|
20547
20782
|
if (!opts.force && exists5) {
|
|
20548
20783
|
skipped++;
|
|
20549
|
-
log.dim(` skipped ${
|
|
20784
|
+
log.dim(` skipped ${path21.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
|
|
20550
20785
|
continue;
|
|
20551
20786
|
}
|
|
20552
|
-
await
|
|
20553
|
-
await
|
|
20787
|
+
await fs22.mkdir(path21.dirname(dest), { recursive: true });
|
|
20788
|
+
await fs22.copyFile(path21.join(srcDir, rel), dest);
|
|
20554
20789
|
copied++;
|
|
20555
|
-
log.dim(` ${
|
|
20790
|
+
log.dim(` ${path21.join(MOTION_DEST, rel)}`);
|
|
20556
20791
|
}
|
|
20557
20792
|
} catch (err) {
|
|
20558
20793
|
log.error(`Copy failed: ${String(err)}`);
|
|
@@ -20593,7 +20828,7 @@ async function motionConstraints(opts, log) {
|
|
|
20593
20828
|
}
|
|
20594
20829
|
const doc = directionConstraint(dir, speed, duration);
|
|
20595
20830
|
const out = opts.out ?? "constraints.json";
|
|
20596
|
-
await
|
|
20831
|
+
await fs22.writeFile(out, JSON.stringify(doc));
|
|
20597
20832
|
if (opts.json) {
|
|
20598
20833
|
writeJson({ out, dir: dirName, speed, duration, waypoints: doc[0].frame_indices.length });
|
|
20599
20834
|
return;
|
|
@@ -20630,14 +20865,14 @@ async function runMotion(opts) {
|
|
|
20630
20865
|
}
|
|
20631
20866
|
|
|
20632
20867
|
// src/commands/blender.ts
|
|
20633
|
-
import
|
|
20634
|
-
import
|
|
20868
|
+
import fs23 from "fs/promises";
|
|
20869
|
+
import path22 from "path";
|
|
20635
20870
|
var SUBS2 = ["demo", "exec", "snap", "scene", "import", "export", "reset", "mcp", "serve", "seat", "release"];
|
|
20636
20871
|
var DEFAULT_OUT_DIR = "assets/blender";
|
|
20637
20872
|
async function writeB64(dir, name, b64) {
|
|
20638
|
-
await
|
|
20639
|
-
const p =
|
|
20640
|
-
await
|
|
20873
|
+
await fs23.mkdir(dir, { recursive: true });
|
|
20874
|
+
const p = path22.join(dir, name);
|
|
20875
|
+
await fs23.writeFile(p, Buffer.from(b64, "base64"));
|
|
20641
20876
|
return p;
|
|
20642
20877
|
}
|
|
20643
20878
|
function reportScene(log, s) {
|
|
@@ -20721,7 +20956,7 @@ async function runBlender(opts) {
|
|
|
20721
20956
|
log.plain(rest.join("\n"));
|
|
20722
20957
|
return 1;
|
|
20723
20958
|
}
|
|
20724
|
-
const outDir =
|
|
20959
|
+
const outDir = path22.resolve(opts.outDir ?? DEFAULT_OUT_DIR);
|
|
20725
20960
|
const mode = opts.mode;
|
|
20726
20961
|
if (mode !== void 0 && !isRenderMode(mode)) {
|
|
20727
20962
|
log.error(`Unknown --mode ${mode}. Use one of: ${RENDER_MODES.join(", ")}.`);
|
|
@@ -20761,14 +20996,14 @@ async function runBlender(opts) {
|
|
|
20761
20996
|
return 0;
|
|
20762
20997
|
}
|
|
20763
20998
|
case "export": {
|
|
20764
|
-
const target = opts.out ??
|
|
20999
|
+
const target = opts.out ?? path22.join(outDir, "scene.glb");
|
|
20765
21000
|
const r = await blenderCall(base, "/export", { path: "/tmp/genex-export.glb" });
|
|
20766
21001
|
if (!r.glbBase64) {
|
|
20767
21002
|
log.error(`/export answered with no GLB bytes${r.uploaded ? " (it was uploaded, not inlined)" : ""}`);
|
|
20768
21003
|
return 1;
|
|
20769
21004
|
}
|
|
20770
|
-
await
|
|
20771
|
-
await
|
|
21005
|
+
await fs23.mkdir(path22.dirname(target), { recursive: true });
|
|
21006
|
+
await fs23.writeFile(target, Buffer.from(r.glbBase64, "base64"));
|
|
20772
21007
|
log.success(`Exported ${r.bytes ?? 0} bytes`);
|
|
20773
21008
|
log.plain(` ${c.cyan(target)}`);
|
|
20774
21009
|
return 0;
|
|
@@ -20801,12 +21036,12 @@ async function runBlender(opts) {
|
|
|
20801
21036
|
return 1;
|
|
20802
21037
|
}
|
|
20803
21038
|
try {
|
|
20804
|
-
script = await
|
|
21039
|
+
script = await fs23.readFile(opts.input, "utf8");
|
|
20805
21040
|
} catch {
|
|
20806
21041
|
log.error(`Can't read ${opts.input}`);
|
|
20807
21042
|
return 1;
|
|
20808
21043
|
}
|
|
20809
|
-
label =
|
|
21044
|
+
label = path22.basename(opts.input);
|
|
20810
21045
|
}
|
|
20811
21046
|
const r = await blenderCall(base, "/exec", { script, ...mode ? { mode } : {}, sheet: { formats: SHEET_FORMATS } });
|
|
20812
21047
|
if (r.stdout?.trim()) log.plain(r.stdout.trimEnd());
|
|
@@ -20925,9 +21160,9 @@ print(f"castle: {n} objects")
|
|
|
20925
21160
|
`;
|
|
20926
21161
|
|
|
20927
21162
|
// src/commands/asset-new.ts
|
|
20928
|
-
import
|
|
21163
|
+
import fs24 from "fs";
|
|
20929
21164
|
import fsp from "fs/promises";
|
|
20930
|
-
import
|
|
21165
|
+
import path23 from "path";
|
|
20931
21166
|
import { pathToFileURL } from "url";
|
|
20932
21167
|
var EXTRA_FILES = [
|
|
20933
21168
|
"genex-asset.example.json",
|
|
@@ -21021,7 +21256,7 @@ Every milestone ends with \`npm run verify\`, then \`npx genex preview --no-buil
|
|
|
21021
21256
|
}
|
|
21022
21257
|
async function runAssetNew(options) {
|
|
21023
21258
|
const log = createLogger();
|
|
21024
|
-
const cwd = options.dir ?
|
|
21259
|
+
const cwd = options.dir ? path23.resolve(options.dir) : process.cwd();
|
|
21025
21260
|
const slug = options.assetSlug;
|
|
21026
21261
|
if (!slug) {
|
|
21027
21262
|
log.error('Usage: genex asset new <slug> [--title "<Name>"] [--dims WxDxH] [--tri-band LOW-HIGH] [--holder "<your name>"] [--out <dir>]');
|
|
@@ -21031,14 +21266,14 @@ async function runAssetNew(options) {
|
|
|
21031
21266
|
log.error(`The slug must be kebab-case (letters, digits, dashes), got: ${slug}`);
|
|
21032
21267
|
return 1;
|
|
21033
21268
|
}
|
|
21034
|
-
const templateDir =
|
|
21035
|
-
if (!
|
|
21269
|
+
const templateDir = path23.join(getTemplatesDir(), "asset-viewer");
|
|
21270
|
+
if (!fs24.existsSync(templateDir)) {
|
|
21036
21271
|
log.error(`Vendored asset-viewer template not found at ${templateDir} \u2014 reinstall the CLI.`);
|
|
21037
21272
|
return 1;
|
|
21038
21273
|
}
|
|
21039
|
-
const manifestTools = await import(pathToFileURL(
|
|
21274
|
+
const manifestTools = await import(pathToFileURL(path23.join(templateDir, "tools", "emit-manifest.mjs")).href);
|
|
21040
21275
|
const { SHARED_FILES, PARITY_FILENAME, hashSharedFiles, pascalCase } = manifestTools;
|
|
21041
|
-
const lockPath =
|
|
21276
|
+
const lockPath = path23.join(templateDir, "shared-files.sha256.json");
|
|
21042
21277
|
const lock = JSON.parse(await fsp.readFile(lockPath, "utf8"));
|
|
21043
21278
|
const actual = hashSharedFiles(templateDir);
|
|
21044
21279
|
const drifted = SHARED_FILES.filter((rel) => lock.files[rel] !== actual[rel]);
|
|
@@ -21051,8 +21286,8 @@ async function runAssetNew(options) {
|
|
|
21051
21286
|
const triBand = parseBand(options.triBand ?? "500-8000");
|
|
21052
21287
|
const holder = options.holder?.trim() || HOLDER_PLACEHOLDER;
|
|
21053
21288
|
const year = (/* @__PURE__ */ new Date()).getUTCFullYear();
|
|
21054
|
-
const outDir =
|
|
21055
|
-
if (
|
|
21289
|
+
const outDir = path23.resolve(cwd, options.out ?? slug);
|
|
21290
|
+
if (fs24.existsSync(outDir) && fs24.readdirSync(outDir).length > 0 && !options.force) {
|
|
21056
21291
|
log.error(`${outDir} already holds files. Nothing was touched \u2014 pass --force to write into it anyway.`);
|
|
21057
21292
|
return 1;
|
|
21058
21293
|
}
|
|
@@ -21080,24 +21315,24 @@ async function runAssetNew(options) {
|
|
|
21080
21315
|
};
|
|
21081
21316
|
await fsp.mkdir(outDir, { recursive: true });
|
|
21082
21317
|
for (const rel of [...SHARED_FILES, ...EXTRA_FILES]) {
|
|
21083
|
-
const to =
|
|
21084
|
-
await fsp.mkdir(
|
|
21085
|
-
await fsp.copyFile(
|
|
21318
|
+
const to = path23.join(outDir, rel);
|
|
21319
|
+
await fsp.mkdir(path23.dirname(to), { recursive: true });
|
|
21320
|
+
await fsp.copyFile(path23.join(templateDir, rel), to);
|
|
21086
21321
|
}
|
|
21087
|
-
const pkg = fillTemplate(await fsp.readFile(
|
|
21322
|
+
const pkg = fillTemplate(await fsp.readFile(path23.join(templateDir, "package.json"), "utf8"), {
|
|
21088
21323
|
slug,
|
|
21089
21324
|
name,
|
|
21090
21325
|
version
|
|
21091
21326
|
});
|
|
21092
|
-
await fsp.writeFile(
|
|
21093
|
-
await fsp.writeFile(
|
|
21094
|
-
await fsp.writeFile(
|
|
21327
|
+
await fsp.writeFile(path23.join(outDir, "package.json"), pkg, "utf8");
|
|
21328
|
+
await fsp.writeFile(path23.join(outDir, "asset.config.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
21329
|
+
await fsp.writeFile(path23.join(outDir, ".gitignore"), GITIGNORE, "utf8");
|
|
21095
21330
|
await fsp.writeFile(
|
|
21096
|
-
|
|
21331
|
+
path23.join(outDir, "DESIGN.md"),
|
|
21097
21332
|
designDoc({ name, slug, sizeMeters, triBand, holder }),
|
|
21098
21333
|
"utf8"
|
|
21099
21334
|
);
|
|
21100
|
-
const placeholder = await fsp.readFile(
|
|
21335
|
+
const placeholder = await fsp.readFile(path23.join(templateDir, "src", "asset", "PLACEHOLDER.ts"), "utf8");
|
|
21101
21336
|
const seeded = seedAssetSource(placeholder, {
|
|
21102
21337
|
slug,
|
|
21103
21338
|
name,
|
|
@@ -21108,8 +21343,8 @@ async function runAssetNew(options) {
|
|
|
21108
21343
|
pascalCase
|
|
21109
21344
|
});
|
|
21110
21345
|
const assetFilename = `${slug.replace(/-([a-z0-9])/g, (_, ch) => ch.toUpperCase())}.ts`;
|
|
21111
|
-
await fsp.mkdir(
|
|
21112
|
-
await fsp.writeFile(
|
|
21346
|
+
await fsp.mkdir(path23.join(outDir, "src", "asset"), { recursive: true });
|
|
21347
|
+
await fsp.writeFile(path23.join(outDir, "src", "asset", assetFilename), seeded, "utf8");
|
|
21113
21348
|
const copied = hashSharedFiles(outDir);
|
|
21114
21349
|
const mismatched = SHARED_FILES.filter((rel) => copied[rel] !== lock.files[rel]);
|
|
21115
21350
|
if (mismatched.length) {
|
|
@@ -21117,7 +21352,7 @@ async function runAssetNew(options) {
|
|
|
21117
21352
|
return 1;
|
|
21118
21353
|
}
|
|
21119
21354
|
await fsp.writeFile(
|
|
21120
|
-
|
|
21355
|
+
path23.join(outDir, PARITY_FILENAME),
|
|
21121
21356
|
JSON.stringify(
|
|
21122
21357
|
{
|
|
21123
21358
|
note: "Recorded by `genex asset new`. Gate G13 recomputes these; do not hand-edit the viewer.",
|
|
@@ -21161,11 +21396,11 @@ async function runAssetNew(options) {
|
|
|
21161
21396
|
}
|
|
21162
21397
|
|
|
21163
21398
|
// src/commands/tools.ts
|
|
21164
|
-
import
|
|
21399
|
+
import path26 from "path";
|
|
21165
21400
|
|
|
21166
21401
|
// src/lib/local-install.ts
|
|
21167
|
-
import
|
|
21168
|
-
import
|
|
21402
|
+
import fs25 from "fs/promises";
|
|
21403
|
+
import path24 from "path";
|
|
21169
21404
|
import { spawn as spawn4 } from "child_process";
|
|
21170
21405
|
var CLI_PACKAGE = "@genex-ai/cli-demo";
|
|
21171
21406
|
var FULL_NAME_FALLBACK = `npx ${CLI_PACKAGE}@${CLI_CHANNEL}`;
|
|
@@ -21178,17 +21413,17 @@ var LOCKFILES = [
|
|
|
21178
21413
|
];
|
|
21179
21414
|
async function exists4(p) {
|
|
21180
21415
|
try {
|
|
21181
|
-
await
|
|
21416
|
+
await fs25.access(p);
|
|
21182
21417
|
return true;
|
|
21183
21418
|
} catch {
|
|
21184
21419
|
return false;
|
|
21185
21420
|
}
|
|
21186
21421
|
}
|
|
21187
21422
|
async function detectPackageManager(cwd) {
|
|
21188
|
-
let dir =
|
|
21423
|
+
let dir = path24.resolve(cwd);
|
|
21189
21424
|
for (; ; ) {
|
|
21190
21425
|
try {
|
|
21191
|
-
const raw = await
|
|
21426
|
+
const raw = await fs25.readFile(path24.join(dir, "package.json"), "utf8");
|
|
21192
21427
|
const pm = JSON.parse(raw).packageManager;
|
|
21193
21428
|
if (typeof pm === "string") {
|
|
21194
21429
|
const name = pm.split("@")[0];
|
|
@@ -21197,18 +21432,18 @@ async function detectPackageManager(cwd) {
|
|
|
21197
21432
|
} catch {
|
|
21198
21433
|
}
|
|
21199
21434
|
for (const [file, pm] of LOCKFILES) {
|
|
21200
|
-
if (await exists4(
|
|
21435
|
+
if (await exists4(path24.join(dir, file))) return pm;
|
|
21201
21436
|
}
|
|
21202
|
-
const parent =
|
|
21437
|
+
const parent = path24.dirname(dir);
|
|
21203
21438
|
if (parent === dir) return "npm";
|
|
21204
21439
|
dir = parent;
|
|
21205
21440
|
}
|
|
21206
21441
|
}
|
|
21207
21442
|
async function findLocalCli(cwd) {
|
|
21208
|
-
let dir =
|
|
21443
|
+
let dir = path24.resolve(cwd);
|
|
21209
21444
|
for (; ; ) {
|
|
21210
|
-
if (await exists4(
|
|
21211
|
-
const parent =
|
|
21445
|
+
if (await exists4(path24.join(dir, "node_modules", CLI_PACKAGE, "package.json"))) return dir;
|
|
21446
|
+
const parent = path24.dirname(dir);
|
|
21212
21447
|
if (parent === dir) return null;
|
|
21213
21448
|
dir = parent;
|
|
21214
21449
|
}
|
|
@@ -21226,7 +21461,7 @@ function installArgs(pm, spec) {
|
|
|
21226
21461
|
}
|
|
21227
21462
|
}
|
|
21228
21463
|
function manifestName(cwd) {
|
|
21229
|
-
const slug =
|
|
21464
|
+
const slug = path24.basename(path24.resolve(cwd)).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 100);
|
|
21230
21465
|
return slug || "genex-tools-workspace";
|
|
21231
21466
|
}
|
|
21232
21467
|
function isSourceRun(moduleUrl = import.meta.url) {
|
|
@@ -21284,10 +21519,10 @@ async function ensureLocalCli(log, cwd = process.cwd(), opts = {}) {
|
|
|
21284
21519
|
return;
|
|
21285
21520
|
}
|
|
21286
21521
|
const pm = await detectPackageManager(cwd);
|
|
21287
|
-
const hadManifest = await exists4(
|
|
21522
|
+
const hadManifest = await exists4(path24.join(cwd, "package.json"));
|
|
21288
21523
|
if (!hadManifest) {
|
|
21289
|
-
await
|
|
21290
|
-
|
|
21524
|
+
await fs25.writeFile(
|
|
21525
|
+
path24.join(cwd, "package.json"),
|
|
21291
21526
|
JSON.stringify({ name: manifestName(cwd), private: true }, null, 2) + "\n"
|
|
21292
21527
|
);
|
|
21293
21528
|
await ensureIgnored(cwd, "node_modules/");
|
|
@@ -21307,20 +21542,20 @@ async function ensureLocalCli(log, cwd = process.cwd(), opts = {}) {
|
|
|
21307
21542
|
}
|
|
21308
21543
|
}
|
|
21309
21544
|
async function ensureIgnored(dir, entry) {
|
|
21310
|
-
const file =
|
|
21545
|
+
const file = path24.join(dir, ".gitignore");
|
|
21311
21546
|
let content = "";
|
|
21312
21547
|
try {
|
|
21313
|
-
content = await
|
|
21548
|
+
content = await fs25.readFile(file, "utf8");
|
|
21314
21549
|
} catch {
|
|
21315
21550
|
}
|
|
21316
21551
|
if (content.split("\n").some((l) => l.trim() === entry)) return;
|
|
21317
21552
|
let next = content;
|
|
21318
21553
|
if (next.length > 0 && !next.endsWith("\n")) next += "\n";
|
|
21319
|
-
await
|
|
21554
|
+
await fs25.writeFile(file, next + entry + "\n");
|
|
21320
21555
|
}
|
|
21321
21556
|
|
|
21322
21557
|
// src/commands/doctor.ts
|
|
21323
|
-
import
|
|
21558
|
+
import path25 from "path";
|
|
21324
21559
|
var LANE_ORDER = [
|
|
21325
21560
|
"model",
|
|
21326
21561
|
"image",
|
|
@@ -21601,7 +21836,7 @@ async function fetchLegalStatus(apiUrl, token) {
|
|
|
21601
21836
|
}
|
|
21602
21837
|
async function firstSkillsMarker() {
|
|
21603
21838
|
for (const target of resolveAgentTargets()) {
|
|
21604
|
-
const marker = await readSkillsMarker(
|
|
21839
|
+
const marker = await readSkillsMarker(path25.join(target.baseDir, "skills"));
|
|
21605
21840
|
if (marker) return marker;
|
|
21606
21841
|
}
|
|
21607
21842
|
return null;
|
|
@@ -21640,8 +21875,8 @@ async function runTools(opts) {
|
|
|
21640
21875
|
let totalNew = 0;
|
|
21641
21876
|
let totalUpdated = 0;
|
|
21642
21877
|
for (const t of targets) {
|
|
21643
|
-
const dest =
|
|
21644
|
-
const { copied, updated } = await copyTemplates(
|
|
21878
|
+
const dest = path26.join(t.baseDir, "skills");
|
|
21879
|
+
const { copied, updated } = await copyTemplates(path26.join(templatesDir, "skills"), dest, {
|
|
21645
21880
|
filter: (rel) => skillFamilyFilter("tools")(`skills/${rel}`)
|
|
21646
21881
|
});
|
|
21647
21882
|
await pruneRemovedSkills(dest, log);
|
|
@@ -21702,7 +21937,7 @@ async function runTools(opts) {
|
|
|
21702
21937
|
// src/lib/costs.ts
|
|
21703
21938
|
var TYPICAL_CREDITS = {
|
|
21704
21939
|
model: 35,
|
|
21705
|
-
// 46 from a reference image
|
|
21940
|
+
// 46 from a reference image; --texture standard 23, --no-texture 12, --geometry detailed 58, --quad 41, --low-poly 46, --parts 58 quote off the estimate
|
|
21706
21941
|
image: 4,
|
|
21707
21942
|
// gpt-image-2 tiers (--transparent, --quality high, --edit, 4K) quote higher
|
|
21708
21943
|
texture: 5,
|
|
@@ -21717,17 +21952,22 @@ var TYPICAL_CREDITS = {
|
|
|
21717
21952
|
model_rig: 29,
|
|
21718
21953
|
model_animation: 12,
|
|
21719
21954
|
// PER CLIP — `--preset walk,run` quotes ceil(2 × 10¢ × 1.15)
|
|
21720
|
-
character:
|
|
21955
|
+
character: 64,
|
|
21956
|
+
// one-shot on Meshy 7 Ultra at 4k with the controller pack; `genex creature` (no pack) 46, --no-ultra 58, --texture 8k 69
|
|
21721
21957
|
character_concept: 32,
|
|
21722
21958
|
character_preview: 41,
|
|
21959
|
+
// Meshy 7 Ultra at 4k; --no-ultra 35, --texture 8k 46
|
|
21723
21960
|
character_finalize: 29,
|
|
21724
21961
|
character_animation: 6,
|
|
21725
21962
|
character_motion: 46,
|
|
21726
21963
|
// one text-route clip; other routes are priced off their own cost
|
|
21727
|
-
character_rerig: 12
|
|
21964
|
+
character_rerig: 12,
|
|
21965
|
+
character_import: 18
|
|
21966
|
+
// Uthana auto-rig of your own GLB; the upload itself is free
|
|
21728
21967
|
};
|
|
21729
21968
|
|
|
21730
21969
|
// src/index.ts
|
|
21970
|
+
var TEXTURE_FLAG_VALUES = ["standard", "detailed", "none", "2k", "4k", "8k"];
|
|
21731
21971
|
var MODEL_SUB_SET = new Set(MODEL_SUBCOMMANDS);
|
|
21732
21972
|
var GEN_KINDS = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "music", "voice", "texture", "image", "video"]);
|
|
21733
21973
|
var HELP = `${c.bold("genex")} \u2014 set up your project's agent workspace, authorize, and publish 3D games.
|
|
@@ -21804,7 +22044,11 @@ ${c.bold("Usage")}
|
|
|
21804
22044
|
default, and --no-resellable turns it back off.
|
|
21805
22045
|
genex model "<prompt>" [options] Generate a 3D model (GLB); prints a public asset URL.
|
|
21806
22046
|
--image <path|url> builds it FROM a reference
|
|
21807
|
-
image instead (prompt optional).
|
|
22047
|
+
image instead (prompt optional). Quality knobs:
|
|
22048
|
+
--texture standard|detailed|none, --geometry
|
|
22049
|
+
detailed, --quad, --low-poly, --parts,
|
|
22050
|
+
--face-limit <n>, --auto-size (each is priced
|
|
22051
|
+
in the quote).
|
|
21808
22052
|
genex model segment <id> Split a generated model into NAMED parts \u2014
|
|
21809
22053
|
one GLB, parts addressable by name (doors,
|
|
21810
22054
|
magazines, turrets, destructibles).
|
|
@@ -21814,6 +22058,10 @@ ${c.bold("Usage")}
|
|
|
21814
22058
|
avian|serpentine|aquatic). Auto-detects the
|
|
21815
22059
|
plan; --type picks it explicitly. Unriggable
|
|
21816
22060
|
meshes are refused and auto-refunded.
|
|
22061
|
+
genex model import <file.glb> Bring in a mesh you already have (a Blender
|
|
22062
|
+
export, a bought asset) as a model of yours \u2014
|
|
22063
|
+
free, \u226464 MB \u2014 so rig/animate/segment, blender
|
|
22064
|
+
import and "Use in game" all work on it.
|
|
21817
22065
|
genex model animate <rig-id> --preset walk[,run,\u2026]
|
|
21818
22066
|
Retarget ready-made motion clips onto a
|
|
21819
22067
|
'model rig' result (billed per clip).
|
|
@@ -21853,6 +22101,12 @@ ${c.bold("Usage")}
|
|
|
21853
22101
|
can take new animations. With no id it adopts the
|
|
21854
22102
|
body this game already wears. Free \u2014 it copies,
|
|
21855
22103
|
it does not generate.
|
|
22104
|
+
genex character import <file.glb> Your own humanoid mesh, auto-rigged by Uthana
|
|
22105
|
+
(finger joints included; --no-fingers skips
|
|
22106
|
+
them; --height <m>). \u226430 MB, biped, T- or
|
|
22107
|
+
A-pose. Then 'character animate <id>
|
|
22108
|
+
--locomotion' \u2014 the Meshy catalog and pack do
|
|
22109
|
+
not apply to a Uthana rig.
|
|
21856
22110
|
genex creature "<desc>" One-shot rigged enemy/creature (biped-shaped
|
|
21857
22111
|
bodies only \u2014 Meshy rig limit): model \u2192 rig \u2192
|
|
21858
22112
|
bind library clips via --animation. No approval
|
|
@@ -21914,6 +22168,14 @@ ${c.bold("Options for the generators (`model` `sfx` `music` `voice` `texture` `i
|
|
|
21914
22168
|
--image <path|url> (model) build the model FROM this reference image \u2014 a
|
|
21915
22169
|
local file (\u22644 MB, inlined) or a previous generation's
|
|
21916
22170
|
asset URL. The prompt becomes optional.
|
|
22171
|
+
--texture <tier> (model) standard | detailed (default) | none \u2014 detailed is
|
|
22172
|
+
+10 credits over standard, none is geometry only.
|
|
22173
|
+
--geometry <tier> (model) standard (default) | detailed (+20 credits, hero pieces).
|
|
22174
|
+
--quad (model) quad-dominant mesh (+5; face limit \u2264150000).
|
|
22175
|
+
--low-poly (model) smart low-poly topology (+10) \u2014 game-ready meshes.
|
|
22176
|
+
--parts (model) separated, named parts at generation (+20).
|
|
22177
|
+
--face-limit <n> (model) cap on the raw mesh, 1000-2000000 (default 150000).
|
|
22178
|
+
--auto-size (model) scale to real-world metres by AI estimate.
|
|
21917
22179
|
--granularity <g> (model segment) part granularity: simple | balanced |
|
|
21918
22180
|
detailed (default balanced).
|
|
21919
22181
|
--type <plan> (model rig) body plan: biped | quadruped | hexapod |
|
|
@@ -22060,13 +22322,23 @@ ${c.bold("Options for `character` / `animations search`")}
|
|
|
22060
22322
|
ranked action IDs instead of choosing silently.
|
|
22061
22323
|
--candidate <1|2|3> (character preview) selected concept candidate.
|
|
22062
22324
|
--user-approved Confirm the user explicitly approved the candidate/preview.
|
|
22063
|
-
--approve-remesh
|
|
22064
|
-
(character finalize) approve the separate
|
|
22065
|
-
|
|
22325
|
+
--approve-remesh <faces>
|
|
22326
|
+
(character finalize) approve the separate rigging copy at this
|
|
22327
|
+
face budget, 10000-100000 (10000 for a crowd body, 30000+ for a
|
|
22328
|
+
hero). The number IS the approval.
|
|
22329
|
+
--direct-text One-shot path: skip concept/preview approval and generate
|
|
22066
22330
|
directly from text (defaults to a 10,000-face target).
|
|
22067
22331
|
--no-controller-pack (character --direct-text only) skip the validated locomotion pack.
|
|
22068
22332
|
--height <meters> (character) target character height, 0.5-3 meters.
|
|
22069
|
-
--polycount <count> (character --direct-text
|
|
22333
|
+
--polycount <count> (character --direct-text / creature) target 10000-100000 polygons.
|
|
22334
|
+
--texture <res> (character/creature/preview) texture size 2k | 4k (default) | 8k
|
|
22335
|
+
(+5 credits). Meshy 7 on every lane.
|
|
22336
|
+
--no-ultra (character/creature/preview) skip Meshy Ultra (\u22125 credits, less
|
|
22337
|
+
surface detail).
|
|
22338
|
+
--pose <a-pose|t-pose>
|
|
22339
|
+
(character --direct-text / creature) preferred rest pose; the
|
|
22340
|
+
other stays the QA fallback.
|
|
22341
|
+
--no-fingers (character import) skip finger joints in the Uthana rig.
|
|
22070
22342
|
--no-wait (character/preview/finalize/animate) enqueue and return a generation id.
|
|
22071
22343
|
--json Emit one machine-readable workflow result object.
|
|
22072
22344
|
--action <id|query> (character animate) action to add; repeatable.
|
|
@@ -22324,6 +22596,13 @@ function parseArgs(argv) {
|
|
|
22324
22596
|
"--image",
|
|
22325
22597
|
"--granularity",
|
|
22326
22598
|
"--preset",
|
|
22599
|
+
// Quality knobs (2026-09-06): `--texture` is shared by `model`
|
|
22600
|
+
// (standard|detailed|none) and the character lanes (2k|4k|8k); each command
|
|
22601
|
+
// re-validates its own vocabulary client-side.
|
|
22602
|
+
"--texture",
|
|
22603
|
+
"--geometry",
|
|
22604
|
+
"--face-limit",
|
|
22605
|
+
"--pose",
|
|
22327
22606
|
// `genex blender` (M1 spike): which shading the contact sheet uses.
|
|
22328
22607
|
"--mode",
|
|
22329
22608
|
// `genex ui` string flags (the numeric ones come from UI_NUMBER_FLAGS).
|
|
@@ -22407,6 +22686,24 @@ function parseArgs(argv) {
|
|
|
22407
22686
|
case "--no-controller-pack":
|
|
22408
22687
|
parsed.options.controllerPack = false;
|
|
22409
22688
|
break;
|
|
22689
|
+
case "--no-fingers":
|
|
22690
|
+
parsed.options.fingers = false;
|
|
22691
|
+
break;
|
|
22692
|
+
case "--no-ultra":
|
|
22693
|
+
parsed.options.ultra = false;
|
|
22694
|
+
break;
|
|
22695
|
+
case "--quad":
|
|
22696
|
+
parsed.options.quad = true;
|
|
22697
|
+
break;
|
|
22698
|
+
case "--low-poly":
|
|
22699
|
+
parsed.options.lowPoly = true;
|
|
22700
|
+
break;
|
|
22701
|
+
case "--parts":
|
|
22702
|
+
parsed.options.parts = true;
|
|
22703
|
+
break;
|
|
22704
|
+
case "--auto-size":
|
|
22705
|
+
parsed.options.autoSize = true;
|
|
22706
|
+
break;
|
|
22410
22707
|
case "--user-approved":
|
|
22411
22708
|
parsed.options.userApproved = true;
|
|
22412
22709
|
break;
|
|
@@ -22527,7 +22824,9 @@ function parseArgs(argv) {
|
|
|
22527
22824
|
parsed.options.conceptId = arg;
|
|
22528
22825
|
} else if (parsed.options.name === "finalize" && !parsed.options.previewId) {
|
|
22529
22826
|
parsed.options.previewId = arg;
|
|
22530
|
-
} else if (
|
|
22827
|
+
} else if (parsed.options.name === "import" && !parsed.options.importPath) {
|
|
22828
|
+
parsed.options.importPath = arg;
|
|
22829
|
+
} else if (!["animate", "motions", "preview", "finalize", "adopt", "import"].includes(parsed.options.name ?? "")) {
|
|
22531
22830
|
parsed.options.name = `${parsed.options.name} ${arg}`;
|
|
22532
22831
|
} else {
|
|
22533
22832
|
parsed.error = `Unexpected argument: ${arg}`;
|
|
@@ -22678,8 +22977,8 @@ function applyValueFlag(options, flag, value) {
|
|
|
22678
22977
|
}
|
|
22679
22978
|
case "--approve-remesh": {
|
|
22680
22979
|
const n = Number(value);
|
|
22681
|
-
if (!Number.isInteger(n) || n
|
|
22682
|
-
throw new Error(`Invalid --approve-remesh value: ${value} (expected
|
|
22980
|
+
if (!Number.isInteger(n) || n < REMESH_TARGET_MIN || n > REMESH_TARGET_MAX) {
|
|
22981
|
+
throw new Error(`Invalid --approve-remesh value: ${value} (expected ${REMESH_TARGET_MIN}-${REMESH_TARGET_MAX} faces)`);
|
|
22683
22982
|
}
|
|
22684
22983
|
options.approveRemesh = n;
|
|
22685
22984
|
break;
|
|
@@ -22847,6 +23146,32 @@ function applyValueFlag(options, flag, value) {
|
|
|
22847
23146
|
case "--image":
|
|
22848
23147
|
options.imageUrl = value;
|
|
22849
23148
|
break;
|
|
23149
|
+
case "--texture":
|
|
23150
|
+
if (!TEXTURE_FLAG_VALUES.includes(value)) {
|
|
23151
|
+
throw new Error(`Invalid --texture value: ${value} (model: standard|detailed|none; character: 2k|4k|8k)`);
|
|
23152
|
+
}
|
|
23153
|
+
options.texture = value;
|
|
23154
|
+
break;
|
|
23155
|
+
case "--geometry":
|
|
23156
|
+
if (value !== "standard" && value !== "detailed") {
|
|
23157
|
+
throw new Error(`Invalid --geometry value: ${value} (expected standard|detailed)`);
|
|
23158
|
+
}
|
|
23159
|
+
options.geometry = value;
|
|
23160
|
+
break;
|
|
23161
|
+
case "--face-limit": {
|
|
23162
|
+
const n = Number(value);
|
|
23163
|
+
if (!Number.isInteger(n) || n < 1e3 || n > 2e6) {
|
|
23164
|
+
throw new Error(`Invalid --face-limit value: ${value} (expected 1000-2000000)`);
|
|
23165
|
+
}
|
|
23166
|
+
options.faceLimit = n;
|
|
23167
|
+
break;
|
|
23168
|
+
}
|
|
23169
|
+
case "--pose":
|
|
23170
|
+
if (value !== "a-pose" && value !== "t-pose") {
|
|
23171
|
+
throw new Error(`Invalid --pose value: ${value} (expected a-pose|t-pose)`);
|
|
23172
|
+
}
|
|
23173
|
+
options.pose = value;
|
|
23174
|
+
break;
|
|
22850
23175
|
case "--granularity":
|
|
22851
23176
|
options.granularity = value;
|
|
22852
23177
|
break;
|
|
@@ -22966,6 +23291,7 @@ async function main() {
|
|
|
22966
23291
|
if (parsed.command === "model" && MODEL_SUB_SET.has(parsed.options.name ?? "")) {
|
|
22967
23292
|
if (parsed.options.name === "segment") await runModelSegment(parsed.options);
|
|
22968
23293
|
else if (parsed.options.name === "rig") await runModelRig(parsed.options);
|
|
23294
|
+
else if (parsed.options.name === "import") await runModelImport(parsed.options);
|
|
22969
23295
|
else await runModelAnimate(parsed.options);
|
|
22970
23296
|
return;
|
|
22971
23297
|
}
|
|
@@ -23031,6 +23357,8 @@ async function main() {
|
|
|
23031
23357
|
await runCharacterFinalize(parsed.options);
|
|
23032
23358
|
} else if (parsed.options.name === "adopt") {
|
|
23033
23359
|
await runCharacterAdopt(parsed.options);
|
|
23360
|
+
} else if (parsed.options.name === "import") {
|
|
23361
|
+
await runCharacterImport(parsed.options);
|
|
23034
23362
|
} else {
|
|
23035
23363
|
await runCharacter({ ...parsed.options, prompt: parsed.options.name });
|
|
23036
23364
|
}
|