@genex-ai/cli-demo 0.86.0-dev.217 → 0.88.0-dev.219
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 +1691 -51
- package/package.json +1 -1
- package/templates/motion/anim-runtime.js +265 -0
- package/templates/motion/ik.js +75 -0
- package/templates/motion/motion.config.json +11 -0
- package/templates/motion/rigs.js +563 -0
- package/templates/motion/sets/jumps.json +1 -0
- package/templates/motion/sets/rifle.json +1 -0
- package/templates/skills/genex-ai-character/SKILL.md +34 -0
- package/templates/skills/genex-ai-character/references/motion.md +145 -0
- package/templates/skills/genex-game-director/SKILL.md +40 -18
- package/templates/skills/genex-game-director/references/design-contract.md +47 -15
- package/templates/skills/genex-threejs-character-controller/SKILL.md +4 -0
- package/templates/skills/genex-threejs-creatures/SKILL.md +5 -0
- package/templates/skills/genex-threejs-procedural-animation/SKILL.md +6 -0
package/dist/index.js
CHANGED
|
@@ -254,10 +254,10 @@ var GENEX_CONTRACT_BLOCK = `${CONTRACT_BEGIN}
|
|
|
254
254
|
|
|
255
255
|
Your capabilities (all via \`npx genex \u2026\`): generate \`model\` \xB7 \`skybox\` \xB7 \`sfx\` \xB7 \`music\` \xB7 \`voice\` \xB7 \`texture\` \xB7 \`image\` (\`--edit\` \xB7 \`--inpaint\` \xB7 \`--glass\` \xB7 \`--clean\` \xB7 \`--upscale\`) \xB7 \`video\` \xB7 rigged \`character\` / \`creature\` \xB7 the pixel toolbox \`ui extract|masks|plate|text-color|trim|audit\` \xB7 vendored \`controller character|car|drone|touch|quality\` \xB7 \`animations search\` \xB7 \`wait <id>\` / \`wait --all\` \xB7 \`preview\` / \`publish\`. Full options: \`npx genex --help\`. Task\u2192lane routing lives in the \`genex-game-director\` skill's routing map \u2014 re-load it whenever you're unsure which lane owns a task.
|
|
256
256
|
|
|
257
|
-
1. Load the \`genex-game-director\` skill and follow it end to end before any game code. After ANY context compaction or session resume, re-read this file
|
|
257
|
+
1. Load the \`genex-game-director\` skill and follow it end to end before any game code. After ANY context compaction or session resume, re-read this file and \`DESIGN.md\`, re-load the skill for the stage you are executing, and continue from the Build plan's \`Now:\` line \u2014 never from memory alone.
|
|
258
258
|
2. The design interview always happens: 2\u20134 build-forking questions, ONE round, before DESIGN.md is written \u2014 even when the request already names the game (then it confirms the pitch). Ask with your built-in question tool with clickable answer options when you have one; a short numbered list in plain chat otherwise. Only after asking may you proceed on stated assumptions if the player stays silent; record each as "assumed \u2014 player didn't answer" in DESIGN.md \u2192 Decisions.
|
|
259
|
-
3. \`DESIGN.md\` at the project root is the design contract. Keep it current; record every decision, assumption, and generation id there.
|
|
260
|
-
4. ALL generated art, audio, video, characters, and UI come from \`genex\` commands \u2014 never from any other generation tool your platform bundles, unless the player explicitly asks for that tool by name. A local reference image is not a reason to switch tools: pass its file path to genex (\`--edit\` and \`--inpaint\` accept local paths).
|
|
259
|
+
3. \`DESIGN.md\` at the project root is the design contract AND the build plan. It must carry a \`## Build plan & status\` section: numbered milestones with status marks and a \`Now:\` line naming the current one \u2014 a milestone is done only when its work reached a preview. Keep it current; record every decision, assumption, and generation id there. When the plan first lands, tell the player in one plain line that the plan is locked in and lives in \`DESIGN.md\`.
|
|
260
|
+
4. ALL generated art, audio, video, characters, and UI come from \`genex\` commands \u2014 never from any other generation tool your platform bundles, unless the player explicitly asks for that tool by name. A local reference image is not a reason to switch tools: pass its file path to genex (\`--edit\` and \`--inpaint\` accept local paths). The same exclusivity covers shipping: building, previewing, and publishing go only through \`genex preview\` / \`genex publish\` \u2014 never load your platform's own site-building, hosting, or deploy skills for this game.
|
|
261
261
|
5. The Stage-1 HUD mockup IS the game concept \u2014 it is your FIRST art action at the UI plan gate. ONE concept image, no candidate variants (generate variants only when the player asks for them), anchored to any reference image the player gave.
|
|
262
262
|
6. The moment the mockup lands: you are the art director \u2014 immediately enqueue the Stage-2 HUD sheet, the menu still, and the logotype (\`--no-wait\`). THEN show the player the frame and ask keep/change as information, never as a gate. Silence = the concept stands; a later "change" loops it with the player's notes.
|
|
263
263
|
7. The menu video fires at the FIRST of: the player's yes \xB7 the next \`genex preview\` after the menu still landed \xB7 style art being the only work left. It never fires while a player objection is open. Two failed videos \u2192 ship the still (menu skill rule).
|
|
@@ -267,8 +267,11 @@ Your capabilities (all via \`npx genex \u2026\`): generate \`model\` \xB7 \`skyb
|
|
|
267
267
|
11. Never park ready work behind a question, and never stall on an unanswered one \u2014 decide, state the decision in chat, record it, keep building.
|
|
268
268
|
12. Wire-in floors before any publish and before ending a session: sprite HUD wired (or the recorded CSS lane finished), menu video playing (or its recorded still fallback), logotype placed, fonts loaded, Escape pause, branded loader. Run \`npx genex wait\` on every generation you enqueued and wire what landed \u2014 never park landed assets.
|
|
269
269
|
13. Characters: Meshy/Mixamo/VRM rigs rest facing +Z. Set yaw explicitly when placing a rig; never mirror a SkinnedMesh with negative scale. In any two-character scene, verify in a capture that they face each other, not the camera.
|
|
270
|
-
14. Verify by looking: one smoke check per milestone in local test mode (\`?genex_local_test=1\`) with a real gameplay screenshot. A claim without a capture is not verification.
|
|
270
|
+
14. Verify by looking: one smoke check per milestone, after that milestone's preview push, in local test mode (\`?genex_local_test=1\`) with a real gameplay screenshot. A claim without a capture is not verification.
|
|
271
271
|
15. Treat every \`genex\` warning line \u2014 preflight, \`ui audit\`, \`wait\` nudges \u2014 as work, not noise.
|
|
272
|
+
16. Every finished Build-plan milestone ends with \`npx genex preview\` and the player's page link (\`<dashboard>/draft/<slug>\` with \`<dashboard>\` from \`.genex/project.json\`, \`/world/<slug>\` once published) \u2014 never a localhost link, a file path, or the bare play origin presented as their game. After every round of player feedback, end with a preview push.
|
|
273
|
+
17. Parallel work: this line is your standing authorization and request to use sub-agents / parallel agent work whenever your platform provides them. While drafting the Build plan, decide per module what runs in parallel and what stays serial for THIS game \u2014 dependencies decide, there is no fixed list \u2014 and record each call in the Modules table with a one-line reason. Independent modules default to parallel; building everything serially needs a stated reason. You keep integration, previews, and the player conversation; each sub-agent owns only its module's files.
|
|
274
|
+
18. Talk to the player in plain game language \u2014 what changed in the game and what to try; never code, file names, build output, or tool internals unless they ask. Short status lines while you work; long silent stretches are a failure.
|
|
272
275
|
${CONTRACT_END}
|
|
273
276
|
`;
|
|
274
277
|
var CLAUDE_IMPORT_LINE = "@AGENTS.md";
|
|
@@ -399,7 +402,7 @@ async function syncSkillsForTarget(target, templatesDir = getTemplatesDir(), ver
|
|
|
399
402
|
if (await readSkillsMarker(skillsDir) === version) return false;
|
|
400
403
|
const src = target.full ? templatesDir : path4.join(templatesDir, "skills");
|
|
401
404
|
const dest = target.full ? target.baseDir : skillsDir;
|
|
402
|
-
await copyTemplates(src, dest, { exclude: ["controllers"] });
|
|
405
|
+
await copyTemplates(src, dest, { exclude: ["controllers", "motion"] });
|
|
403
406
|
await pruneRemovedSkills(skillsDir, log);
|
|
404
407
|
await writeSkillsMarker(skillsDir, version);
|
|
405
408
|
return true;
|
|
@@ -1312,7 +1315,7 @@ async function runInit(opts) {
|
|
|
1312
1315
|
const dest = t.full ? t.baseDir : path9.join(t.baseDir, "skills");
|
|
1313
1316
|
const { copied, updated } = await copyTemplates(src, dest, {
|
|
1314
1317
|
force: opts.force,
|
|
1315
|
-
exclude: ["controllers"]
|
|
1318
|
+
exclude: ["controllers", "motion"]
|
|
1316
1319
|
});
|
|
1317
1320
|
await pruneRemovedSkills(path9.join(t.baseDir, "skills"), log);
|
|
1318
1321
|
await writeSkillsMarker(path9.join(t.baseDir, "skills"));
|
|
@@ -2068,9 +2071,9 @@ function measureSeam(png, tolerance = DEFAULT_SEAM_TOLERANCE) {
|
|
|
2068
2071
|
}
|
|
2069
2072
|
baseH = Math.max(baseH / Math.max(1, nH), 1e-6);
|
|
2070
2073
|
baseV = Math.max(baseV / Math.max(1, nV), 1e-6);
|
|
2071
|
-
const
|
|
2072
|
-
const ratioHorizontal =
|
|
2073
|
-
const ratioVertical =
|
|
2074
|
+
const round2 = (n) => Math.round(n * 100) / 100;
|
|
2075
|
+
const ratioHorizontal = round2(seamH / baseH);
|
|
2076
|
+
const ratioVertical = round2(seamV / baseV);
|
|
2074
2077
|
const worstRatio = Math.max(ratioHorizontal, ratioVertical);
|
|
2075
2078
|
return {
|
|
2076
2079
|
width: w,
|
|
@@ -3603,11 +3606,11 @@ async function uiAudit(opts, log) {
|
|
|
3603
3606
|
return;
|
|
3604
3607
|
}
|
|
3605
3608
|
const hard = findings.filter((finding) => HARD_AUDIT_KINDS.has(finding.kind));
|
|
3606
|
-
const
|
|
3609
|
+
const soft2 = findings.filter((finding) => !HARD_AUDIT_KINDS.has(finding.kind));
|
|
3607
3610
|
for (const finding of hard) log.error(`[${finding.kind}] ${finding.message}`);
|
|
3608
|
-
if (
|
|
3609
|
-
log.warn(`${
|
|
3610
|
-
for (const finding of
|
|
3611
|
+
if (soft2.length > 0) {
|
|
3612
|
+
log.warn(`${soft2.length} finding(s). These are heuristics \u2014 check each one; some may be fine:`);
|
|
3613
|
+
for (const finding of soft2) log.warn(` [${finding.kind}] ${finding.message}`);
|
|
3611
3614
|
}
|
|
3612
3615
|
if (opts.strict || hard.length > 0) process.exitCode = 1;
|
|
3613
3616
|
}
|
|
@@ -4256,6 +4259,8 @@ var WAIT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
|
4256
4259
|
var KIND_WAIT_TIMEOUT_MS = {
|
|
4257
4260
|
video: 15 * 60 * 1e3,
|
|
4258
4261
|
// 15 min
|
|
4262
|
+
motion: 20 * 60 * 1e3,
|
|
4263
|
+
// 8 takes ≈ 8-9 GPU min + a serverless cold start
|
|
4259
4264
|
character_concept: 15 * 60 * 1e3,
|
|
4260
4265
|
character_preview: 30 * 60 * 1e3,
|
|
4261
4266
|
character: 30 * 60 * 1e3,
|
|
@@ -4371,6 +4376,7 @@ function printHint(kind, view, files, log) {
|
|
|
4371
4376
|
texture: `Load each map with TextureLoader (RepeatWrapping) into a MeshStandardMaterial \u2014 see genex-ai-texture. Use the URLs above by role.`,
|
|
4372
4377
|
image: `Load with TextureLoader (set colorSpace = SRGBColorSpace) onto any mesh/plane/sprite \u2014 see genex-ai-image. url = "${url}"`,
|
|
4373
4378
|
video: `Wire an HTMLVideoElement (crossOrigin="anonymous", muted, loop, playsInline) into a THREE.VideoTexture \u2014 see genex-ai-video. url = "${url}"`,
|
|
4379
|
+
motion: `Raw ARDY takes + verify report \u2014 download the takes, rename them meaningfully (routing is by filename), then: genex motion verify \u2192 compile \u2192 install. Full procedure: the genex-ai-character skill's motion reference.`,
|
|
4374
4380
|
character_concept: "Show all three concept candidates to the user and wait for an explicit selection.",
|
|
4375
4381
|
character_preview: "Show all four preview views to the user and wait for explicit approval before finalizing.",
|
|
4376
4382
|
character: `Install the controller and current manifest with genex controller character --character ${view.id}.`,
|
|
@@ -4641,6 +4647,11 @@ async function printPipelineStatePreflight(log, cwd = process.cwd()) {
|
|
|
4641
4647
|
'Menu video not enqueued \u2014 the animated menu backdrop is the default for every game. Enqueue it (genex-ai-menu), or record "Menu video: declined (reason)" in DESIGN.md if the player declined it.'
|
|
4642
4648
|
);
|
|
4643
4649
|
}
|
|
4650
|
+
if (design.length > 0 && !/##\s*build plan & status/i.test(design)) {
|
|
4651
|
+
warnings.push(
|
|
4652
|
+
"DESIGN.md has no `## Build plan & status` section \u2014 numbered milestones with a `Now:` line are how a compacted or resumed session knows where the build is (genex-game-director's design contract has the shape)."
|
|
4653
|
+
);
|
|
4654
|
+
}
|
|
4644
4655
|
if (!/hud lane:/i.test(design)) {
|
|
4645
4656
|
const hudDom = await hasHudLikeDom(cwd);
|
|
4646
4657
|
if (hudDom && !await hasExtractionSidecars(cwd)) {
|
|
@@ -5763,6 +5774,11 @@ async function runWait(opts) {
|
|
|
5763
5774
|
await reportTerminal(kind, view, log, opts.open, opts.json);
|
|
5764
5775
|
return;
|
|
5765
5776
|
}
|
|
5777
|
+
if (kind === "character_concept" || kind === "character_preview" || kind === "character") {
|
|
5778
|
+
log.dim(
|
|
5779
|
+
` \u23F3 Character stages take several minutes server-side \u2014 you don't have to sit here. Keep building and check back with ${c.cyan("genex wait --all")}.`
|
|
5780
|
+
);
|
|
5781
|
+
}
|
|
5766
5782
|
await awaitAndReport(apiUrl, token, id, kind, log, opts.open, opts.json);
|
|
5767
5783
|
}
|
|
5768
5784
|
async function runWaitAll(opts) {
|
|
@@ -5851,6 +5867,10 @@ async function runWaitAll(opts) {
|
|
|
5851
5867
|
log.plain("");
|
|
5852
5868
|
log.plain(` ${c.bold(STAGE1_LANDING_NUDGE)}`);
|
|
5853
5869
|
}
|
|
5870
|
+
log.plain("");
|
|
5871
|
+
log.dim(
|
|
5872
|
+
" \u{1F4DC} Resumed or compacted? Re-read AGENTS.md + DESIGN.md (continue from its Now: line) before building on."
|
|
5873
|
+
);
|
|
5854
5874
|
}
|
|
5855
5875
|
async function toRow(e, v, cwd) {
|
|
5856
5876
|
const base = { id: e.id, kind: e.kind, prompt: e.prompt, queuedAt: e.queuedAt };
|
|
@@ -14969,17 +14989,17 @@ function searchMeshyAnimations(query, options = {}) {
|
|
|
14969
14989
|
if (options.category && entry.category.toLowerCase() !== options.category.toLowerCase()) continue;
|
|
14970
14990
|
if (options.inPlace !== void 0 && entry.inPlace !== options.inPlace) continue;
|
|
14971
14991
|
const matched = [];
|
|
14972
|
-
let
|
|
14992
|
+
let score2 = 0;
|
|
14973
14993
|
if (String(entry.actionId) === rawQuery) {
|
|
14974
|
-
|
|
14994
|
+
score2 += 1e5;
|
|
14975
14995
|
matched.push("action id");
|
|
14976
14996
|
}
|
|
14977
14997
|
if (entry.key.toLowerCase() === rawQuery) {
|
|
14978
|
-
|
|
14998
|
+
score2 += 8e4;
|
|
14979
14999
|
matched.push("stable key");
|
|
14980
15000
|
}
|
|
14981
15001
|
if (entry.name.toLowerCase() === rawQuery) {
|
|
14982
|
-
|
|
15002
|
+
score2 += 6e4;
|
|
14983
15003
|
matched.push("exact name");
|
|
14984
15004
|
}
|
|
14985
15005
|
const haystack = new Set(tokens([
|
|
@@ -14997,13 +15017,13 @@ function searchMeshyAnimations(query, options = {}) {
|
|
|
14997
15017
|
coveredGroups++;
|
|
14998
15018
|
const original = queryTokens[index];
|
|
14999
15019
|
const best = hits.includes(original) ? original : hits[0];
|
|
15000
|
-
|
|
15020
|
+
score2 += best === original ? 400 : 140;
|
|
15001
15021
|
matched.push(best);
|
|
15002
15022
|
}
|
|
15003
|
-
if (entry.inPlace && (expanded.has("walk") || expanded.has("run")))
|
|
15004
|
-
const exact =
|
|
15005
|
-
if (
|
|
15006
|
-
results.push({ entry, score, matched: [...new Set(matched)] });
|
|
15023
|
+
if (entry.inPlace && (expanded.has("walk") || expanded.has("run"))) score2 += 35;
|
|
15024
|
+
const exact = score2 >= 6e4;
|
|
15025
|
+
if (score2 > 0 && (exact || coveredGroups === groups.length)) {
|
|
15026
|
+
results.push({ entry, score: score2, matched: [...new Set(matched)] });
|
|
15007
15027
|
}
|
|
15008
15028
|
}
|
|
15009
15029
|
return results.sort((a, b) => b.score - a.score || a.entry.name.localeCompare(b.entry.name) || a.entry.actionId - b.entry.actionId).slice(0, options.limit ?? 20);
|
|
@@ -15587,9 +15607,9 @@ async function installMeshyCharacterManifest(args) {
|
|
|
15587
15607
|
`
|
|
15588
15608
|
);
|
|
15589
15609
|
args.log.dim(` public/assets/meshy-character.json (${args.characterId}, current revision)`);
|
|
15590
|
-
const
|
|
15591
|
-
if (typeof
|
|
15592
|
-
args.log.success(`Meshy controller pack ${
|
|
15610
|
+
const pack2 = manifest.controllerPack;
|
|
15611
|
+
if (typeof pack2?.key === "string" && typeof pack2.version === "number") {
|
|
15612
|
+
args.log.success(`Meshy controller pack ${pack2.key} v${pack2.version}`);
|
|
15593
15613
|
} else {
|
|
15594
15614
|
args.log.warn("Legacy Meshy manifest \u2014 regenerate the character for the immutable preview-reviewed neutral-v3 locomotion pack.");
|
|
15595
15615
|
}
|
|
@@ -15650,37 +15670,37 @@ function incompletePack(message) {
|
|
|
15650
15670
|
throw new Error(`Incomplete Meshy controller pack: ${message}`);
|
|
15651
15671
|
}
|
|
15652
15672
|
function assertCompleteMeshyControllerPack(manifest) {
|
|
15653
|
-
const
|
|
15654
|
-
if (
|
|
15655
|
-
if (typeof
|
|
15673
|
+
const pack2 = manifest.controllerPack;
|
|
15674
|
+
if (pack2 === void 0) return;
|
|
15675
|
+
if (typeof pack2.key !== "string" || pack2.key.length === 0 || typeof pack2.version !== "number" || !Number.isInteger(pack2.version)) {
|
|
15656
15676
|
incompletePack("invalid key or version.");
|
|
15657
15677
|
}
|
|
15658
|
-
if (typeof
|
|
15659
|
-
incompletePack(`${
|
|
15678
|
+
if (typeof pack2.fingerprint !== "string" || pack2.fingerprint.length === 0) {
|
|
15679
|
+
incompletePack(`${pack2.key} v${pack2.version} has no immutable fingerprint.`);
|
|
15660
15680
|
}
|
|
15661
|
-
if (!Array.isArray(
|
|
15662
|
-
incompletePack(`${
|
|
15681
|
+
if (!Array.isArray(pack2.actionIds) || pack2.actionIds.length === 0 || pack2.actionIds.some((actionId) => typeof actionId !== "number" || !Number.isInteger(actionId))) {
|
|
15682
|
+
incompletePack(`${pack2.key} v${pack2.version} has no valid action snapshot.`);
|
|
15663
15683
|
}
|
|
15664
|
-
const actionIds =
|
|
15684
|
+
const actionIds = pack2.actionIds;
|
|
15665
15685
|
const packActionIds = new Set(actionIds);
|
|
15666
15686
|
if (packActionIds.size !== actionIds.length) {
|
|
15667
|
-
incompletePack(`${
|
|
15687
|
+
incompletePack(`${pack2.key} v${pack2.version} repeats an action ID.`);
|
|
15668
15688
|
}
|
|
15669
15689
|
const bindings = manifest.locomotion?.bindings;
|
|
15670
|
-
if (!bindings) incompletePack(`${
|
|
15690
|
+
if (!bindings) incompletePack(`${pack2.key} v${pack2.version} has no authoritative locomotion bindings.`);
|
|
15671
15691
|
const missingSlots = REQUIRED_MESHY_CONTROLLER_SLOTS.filter((slot) => bindings[slot] === void 0);
|
|
15672
15692
|
if (missingSlots.length > 0) {
|
|
15673
|
-
incompletePack(`${
|
|
15693
|
+
incompletePack(`${pack2.key} v${pack2.version} is missing ${missingSlots.join(", ")}.`);
|
|
15674
15694
|
}
|
|
15675
15695
|
const clips = manifest.clips ?? [];
|
|
15676
15696
|
const boundActionIds = /* @__PURE__ */ new Set();
|
|
15677
15697
|
const snapshotBindings = {};
|
|
15678
15698
|
for (const [slot, binding] of Object.entries(bindings)) {
|
|
15679
15699
|
if (typeof binding.actionId !== "number" || !Number.isInteger(binding.actionId) || typeof binding.clip !== "string" || binding.clip.length === 0 || binding.mode !== "loop" && binding.mode !== "one-shot" && binding.mode !== "pose") {
|
|
15680
|
-
incompletePack(`${
|
|
15700
|
+
incompletePack(`${pack2.key} v${pack2.version} has an invalid ${slot} binding.`);
|
|
15681
15701
|
}
|
|
15682
15702
|
if (binding.phase !== void 0 && (typeof binding.phase !== "number" || !Number.isFinite(binding.phase) || binding.phase < 0 || binding.phase > 1)) {
|
|
15683
|
-
incompletePack(`${
|
|
15703
|
+
incompletePack(`${pack2.key} v${pack2.version} has an invalid ${slot} phase.`);
|
|
15684
15704
|
}
|
|
15685
15705
|
const actionId = binding.actionId;
|
|
15686
15706
|
boundActionIds.add(actionId);
|
|
@@ -15699,21 +15719,21 @@ function assertCompleteMeshyControllerPack(manifest) {
|
|
|
15699
15719
|
}
|
|
15700
15720
|
const unboundActionIds = actionIds.filter((actionId) => !boundActionIds.has(actionId));
|
|
15701
15721
|
if (unboundActionIds.length > 0) {
|
|
15702
|
-
incompletePack(`${
|
|
15722
|
+
incompletePack(`${pack2.key} v${pack2.version} action snapshot contains unbound actions ${unboundActionIds.join(", ")}.`);
|
|
15703
15723
|
}
|
|
15704
15724
|
let computedFingerprint;
|
|
15705
15725
|
try {
|
|
15706
15726
|
computedFingerprint = createMeshyControllerPackSnapshot({
|
|
15707
|
-
key:
|
|
15708
|
-
version:
|
|
15727
|
+
key: pack2.key,
|
|
15728
|
+
version: pack2.version,
|
|
15709
15729
|
actionIds,
|
|
15710
15730
|
bindings: snapshotBindings
|
|
15711
15731
|
}).fingerprint;
|
|
15712
15732
|
} catch (error) {
|
|
15713
15733
|
incompletePack(error instanceof Error ? error.message : "the immutable snapshot is invalid.");
|
|
15714
15734
|
}
|
|
15715
|
-
if (computedFingerprint !==
|
|
15716
|
-
incompletePack(`${
|
|
15735
|
+
if (computedFingerprint !== pack2.fingerprint) {
|
|
15736
|
+
incompletePack(`${pack2.key} v${pack2.version} fingerprint does not match its action bindings.`);
|
|
15717
15737
|
}
|
|
15718
15738
|
}
|
|
15719
15739
|
async function installOwnerAvatar(args) {
|
|
@@ -15787,6 +15807,22 @@ async function context(opts) {
|
|
|
15787
15807
|
const project = await readProject();
|
|
15788
15808
|
return { token, apiUrl: getApiUrl(opts.apiUrl ?? project?.apiUrl) };
|
|
15789
15809
|
}
|
|
15810
|
+
var CHARACTER_BRIEF_MAX_CHARS = 600;
|
|
15811
|
+
function apiErrorMessage(data, fallback) {
|
|
15812
|
+
if (typeof data !== "object" || data === null) return fallback;
|
|
15813
|
+
const body = data;
|
|
15814
|
+
if (typeof body.message === "string" && body.message) return body.message;
|
|
15815
|
+
if (typeof body.error === "string" && body.error) return body.error;
|
|
15816
|
+
const err = body.error;
|
|
15817
|
+
if (err && Array.isArray(err.issues) && err.issues.length > 0) {
|
|
15818
|
+
const lines = err.issues.map((i) => {
|
|
15819
|
+
const where = Array.isArray(i.path) && i.path.length > 0 ? `${i.path.join(".")}: ` : "";
|
|
15820
|
+
return typeof i.message === "string" ? `${where}${i.message}` : "";
|
|
15821
|
+
}).filter(Boolean);
|
|
15822
|
+
if (lines.length > 0) return `${fallback} ${lines.join("; ")}`;
|
|
15823
|
+
}
|
|
15824
|
+
return fallback;
|
|
15825
|
+
}
|
|
15790
15826
|
async function quote(url, token, body) {
|
|
15791
15827
|
const response = await apiFetch(url, {
|
|
15792
15828
|
method: "POST",
|
|
@@ -15796,7 +15832,7 @@ async function quote(url, token, body) {
|
|
|
15796
15832
|
if (printedStructuredError(response)) return null;
|
|
15797
15833
|
if (!response.ok) {
|
|
15798
15834
|
const data = await response.json().catch(() => ({}));
|
|
15799
|
-
throw new Error(data
|
|
15835
|
+
throw new Error(apiErrorMessage(data, `Quote failed (HTTP ${response.status}).`));
|
|
15800
15836
|
}
|
|
15801
15837
|
return (await response.json()).quote;
|
|
15802
15838
|
}
|
|
@@ -15848,7 +15884,7 @@ async function postWorkflow(url, token, body) {
|
|
|
15848
15884
|
if (printedStructuredError(response)) return null;
|
|
15849
15885
|
if (!response.ok) {
|
|
15850
15886
|
const data2 = await response.json().catch(() => ({}));
|
|
15851
|
-
throw new Error(data2
|
|
15887
|
+
throw new Error(apiErrorMessage(data2, `Character workflow request failed (HTTP ${response.status}).`));
|
|
15852
15888
|
}
|
|
15853
15889
|
const data = await response.json();
|
|
15854
15890
|
if (typeof data.id !== "string" || data.id.length === 0) {
|
|
@@ -15909,6 +15945,14 @@ async function runCharacterConcept(opts) {
|
|
|
15909
15945
|
fail2(opts, 'Missing brief. Usage: genex character "<brief>".', "character_concept");
|
|
15910
15946
|
return;
|
|
15911
15947
|
}
|
|
15948
|
+
if (prompt.length > CHARACTER_BRIEF_MAX_CHARS) {
|
|
15949
|
+
fail2(
|
|
15950
|
+
opts,
|
|
15951
|
+
`The brief is ${prompt.length} characters \u2014 the character concept endpoint caps it at ${CHARACTER_BRIEF_MAX_CHARS}. Tighten it to the look that matters (silhouette, materials, palette, mood); gameplay details don't belong in the brief.`,
|
|
15952
|
+
"character_concept"
|
|
15953
|
+
);
|
|
15954
|
+
return;
|
|
15955
|
+
}
|
|
15912
15956
|
if (opts.polycount !== void 0) {
|
|
15913
15957
|
fail2(opts, "--polycount is only available with --direct-text. The approval flow remeshes only after finalize approval.", "character_concept");
|
|
15914
15958
|
return;
|
|
@@ -16334,16 +16378,1550 @@ function rank(items, query) {
|
|
|
16334
16378
|
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
16335
16379
|
return items.map((p) => {
|
|
16336
16380
|
const hay = `${p.title} ${p.description} ${p.categories.join(" ")}`.toLowerCase();
|
|
16337
|
-
let
|
|
16381
|
+
let score2 = 0;
|
|
16338
16382
|
for (const t of terms) {
|
|
16339
|
-
if (p.title.toLowerCase().includes(t))
|
|
16340
|
-
if (p.categories.some((cat) => cat.includes(t)))
|
|
16341
|
-
if (hay.includes(t))
|
|
16383
|
+
if (p.title.toLowerCase().includes(t)) score2 += 3;
|
|
16384
|
+
if (p.categories.some((cat) => cat.includes(t))) score2 += 2;
|
|
16385
|
+
if (hay.includes(t)) score2 += 1;
|
|
16342
16386
|
}
|
|
16343
|
-
return { p, score };
|
|
16387
|
+
return { p, score: score2 };
|
|
16344
16388
|
}).filter((x) => x.score > 0).sort((a, b) => b.score - a.score || b.p.playsCount - a.p.playsCount).map((x) => x.p);
|
|
16345
16389
|
}
|
|
16346
16390
|
|
|
16391
|
+
// src/commands/motion.ts
|
|
16392
|
+
import fs18 from "fs/promises";
|
|
16393
|
+
import path18 from "path";
|
|
16394
|
+
|
|
16395
|
+
// src/lib/motion/npz.ts
|
|
16396
|
+
import zlib from "zlib";
|
|
16397
|
+
function readNpy(b) {
|
|
16398
|
+
if (b.toString("latin1", 1, 6) !== "NUMPY") throw new Error("not an .npy member");
|
|
16399
|
+
const major = b[6];
|
|
16400
|
+
const hlen = major >= 2 ? b.readUInt32LE(8) : b.readUInt16LE(8);
|
|
16401
|
+
const hstart = major >= 2 ? 12 : 10;
|
|
16402
|
+
const header = b.toString("latin1", hstart, hstart + hlen);
|
|
16403
|
+
const descr = /'descr':\s*'([^']+)'/.exec(header)?.[1];
|
|
16404
|
+
const fortran = /'fortran_order':\s*(True|False)/.exec(header)?.[1] === "True";
|
|
16405
|
+
const shapeRaw = /'shape':\s*\(([^)]*)\)/.exec(header)?.[1];
|
|
16406
|
+
if (!descr || shapeRaw === void 0) throw new Error(`unparseable .npy header: ${header}`);
|
|
16407
|
+
if (fortran) throw new Error("fortran-order .npy is unsupported");
|
|
16408
|
+
const shape = shapeRaw.split(",").map((s) => s.trim()).filter(Boolean).map(Number);
|
|
16409
|
+
const data = Buffer.from(b.subarray(hstart + hlen));
|
|
16410
|
+
if (descr === "<f4") return { shape, descr, data: new Float32Array(data.buffer, data.byteOffset, data.length / 4) };
|
|
16411
|
+
if (descr === "<f8") return { shape, descr, data: new Float64Array(data.buffer, data.byteOffset, data.length / 8) };
|
|
16412
|
+
if (descr === "<i8") return { shape, descr, data: new BigInt64Array(data.buffer, data.byteOffset, data.length / 8) };
|
|
16413
|
+
if (descr === "|b1") return { shape, descr, data: new Uint8Array(data.buffer, data.byteOffset, data.length) };
|
|
16414
|
+
if (descr.startsWith("<U")) return { shape, descr, data: data.toString("utf16le").replace(/\0+$/, "") };
|
|
16415
|
+
if (descr === "|O") throw new Error(".npz member is a pickled object array \u2014 regenerate the take with a current ARDY");
|
|
16416
|
+
throw new Error(`unsupported .npy dtype ${descr}`);
|
|
16417
|
+
}
|
|
16418
|
+
function readNpz(buf) {
|
|
16419
|
+
let e = buf.length - 22;
|
|
16420
|
+
while (e >= 0 && buf.readUInt32LE(e) !== 101010256) e--;
|
|
16421
|
+
if (e < 0) throw new Error("not a ZIP/.npz file (no end-of-central-directory)");
|
|
16422
|
+
const count = buf.readUInt16LE(e + 10);
|
|
16423
|
+
let off = buf.readUInt32LE(e + 16);
|
|
16424
|
+
const out = {};
|
|
16425
|
+
for (let i = 0; i < count; i++) {
|
|
16426
|
+
if (buf.readUInt32LE(off) !== 33639248) throw new Error("corrupt ZIP central directory");
|
|
16427
|
+
const method = buf.readUInt16LE(off + 10);
|
|
16428
|
+
const csize = buf.readUInt32LE(off + 20);
|
|
16429
|
+
const nameLen = buf.readUInt16LE(off + 28);
|
|
16430
|
+
const extraLen = buf.readUInt16LE(off + 30);
|
|
16431
|
+
const cmtLen = buf.readUInt16LE(off + 32);
|
|
16432
|
+
const lho = buf.readUInt32LE(off + 42);
|
|
16433
|
+
const name = buf.toString("utf8", off + 46, off + 46 + nameLen);
|
|
16434
|
+
const lNameLen = buf.readUInt16LE(lho + 26);
|
|
16435
|
+
const lExtraLen = buf.readUInt16LE(lho + 28);
|
|
16436
|
+
const dataStart = lho + 30 + lNameLen + lExtraLen;
|
|
16437
|
+
const raw = buf.subarray(dataStart, dataStart + csize);
|
|
16438
|
+
const bytes = method === 8 ? zlib.inflateRawSync(raw) : Buffer.from(raw);
|
|
16439
|
+
if (method !== 0 && method !== 8) throw new Error(`unsupported ZIP method ${method} for ${name}`);
|
|
16440
|
+
out[name.replace(/\.npy$/, "")] = readNpy(bytes);
|
|
16441
|
+
off += 46 + nameLen + extraLen + cmtLen;
|
|
16442
|
+
}
|
|
16443
|
+
return out;
|
|
16444
|
+
}
|
|
16445
|
+
function floats(npz, key) {
|
|
16446
|
+
const m = npz[key];
|
|
16447
|
+
if (!m) throw new Error(`.npz is missing "${key}" \u2014 not an ARDY take?`);
|
|
16448
|
+
const d = m.data;
|
|
16449
|
+
if (d instanceof Float64Array) return { shape: m.shape, a: d };
|
|
16450
|
+
if (d instanceof Float32Array) return { shape: m.shape, a: Float64Array.from(d) };
|
|
16451
|
+
if (d instanceof Uint8Array && m.descr === "|b1") return { shape: m.shape, a: Float64Array.from(d) };
|
|
16452
|
+
throw new Error(`"${key}" has dtype ${m.descr}, expected floats`);
|
|
16453
|
+
}
|
|
16454
|
+
|
|
16455
|
+
// src/lib/motion/core27.ts
|
|
16456
|
+
var JOINTS = ["Hips", "Spine", "Spine1", "Spine2", "Spine3", "Neck", "Head", "RightShoulder", "RightArm", "RightForeArm", "RightHand", "RightHandEnd", "RightHandThumb1", "LeftShoulder", "LeftArm", "LeftForeArm", "LeftHand", "LeftHandEnd", "LeftHandThumb1", "RightUpLeg", "RightLeg", "RightFoot", "RightToeBase", "LeftUpLeg", "LeftLeg", "LeftFoot", "LeftToeBase"];
|
|
16457
|
+
var PARENTS = [-1, 0, 1, 2, 3, 4, 5, 4, 7, 8, 9, 10, 10, 4, 13, 14, 15, 16, 16, 0, 19, 20, 21, 0, 23, 24, 25];
|
|
16458
|
+
var REST_OFFSETS = [[0, 0, 0], [0, 0.071, -0.0473], [0, 0.0932, -0.0164], [0, 0.0943, -82e-4], [0, 0.0947, -0], [0, 0.2485, 0.0355], [-0, 0.1282, 0.0226], [-0.032, 0.1728, 0.0533], [-0.1589, -0, 0], [-0.2954, -0, -0], [-0.2327, -0, -0], [-0.0696, -0, 0], [-0.0278, -0.0186, 0.0464], [0.032, 0.1728, 0.0533], [0.1589, -0, -0], [0.2954, 0, 0], [0.2327, -0, 0], [0.0696, 0, 0], [0.0278, -0.0186, 0.0464], [-0.0949, -0.0277, -0], [-0, -0.4121, -0], [0, -0.4561, -0], [0, -0.0585, 0.1607], [0.0949, -0.0277, 0], [-0, -0.4121, 0], [-0, -0.4561, -0], [0, -0.0585, 0.1607]];
|
|
16459
|
+
var REST_POSITIONS = [[0, 0, 0], [0, 0.071, -0.0473], [0, 0.1642, -0.0637], [0, 0.2585, -0.0719], [0, 0.3532, -0.0719], [0, 0.6017, -0.0364], [0, 0.7299, -0.0138], [-0.032, 0.526, -0.0186], [-0.1909, 0.526, -0.0186], [-0.4863, 0.526, -0.0186], [-0.719, 0.526, -0.0186], [-0.7886, 0.526, -0.0186], [-0.7468, 0.5074, 0.0278], [0.032, 0.526, -0.0186], [0.1909, 0.526, -0.0186], [0.4863, 0.526, -0.0186], [0.719, 0.526, -0.0186], [0.7886, 0.526, -0.0186], [0.7468, 0.5074, 0.0278], [-0.0949, -0.0277, 0], [-0.0949, -0.4398, 0], [-0.0949, -0.8959, 0], [-0.0949, -0.9544, 0.1607], [0.0949, -0.0277, 0], [0.0949, -0.4398, 0], [0.0949, -0.8959, 0], [0.0949, -0.9544, 0.1607]];
|
|
16460
|
+
var HIPS_REST_Y = 0.9544;
|
|
16461
|
+
var J = JOINTS.length;
|
|
16462
|
+
|
|
16463
|
+
// src/lib/motion/skeleton.ts
|
|
16464
|
+
var ji = (name) => {
|
|
16465
|
+
const i = JOINTS.indexOf(name);
|
|
16466
|
+
if (i < 0) throw new Error(`unknown CoreSkeleton27 joint ${name}`);
|
|
16467
|
+
return i;
|
|
16468
|
+
};
|
|
16469
|
+
function matMul(a, b) {
|
|
16470
|
+
return [
|
|
16471
|
+
a[0] * b[0] + a[1] * b[3] + a[2] * b[6],
|
|
16472
|
+
a[0] * b[1] + a[1] * b[4] + a[2] * b[7],
|
|
16473
|
+
a[0] * b[2] + a[1] * b[5] + a[2] * b[8],
|
|
16474
|
+
a[3] * b[0] + a[4] * b[3] + a[5] * b[6],
|
|
16475
|
+
a[3] * b[1] + a[4] * b[4] + a[5] * b[7],
|
|
16476
|
+
a[3] * b[2] + a[4] * b[5] + a[5] * b[8],
|
|
16477
|
+
a[6] * b[0] + a[7] * b[3] + a[8] * b[6],
|
|
16478
|
+
a[6] * b[1] + a[7] * b[4] + a[8] * b[7],
|
|
16479
|
+
a[6] * b[2] + a[7] * b[5] + a[8] * b[8]
|
|
16480
|
+
];
|
|
16481
|
+
}
|
|
16482
|
+
function matVec(m, v) {
|
|
16483
|
+
return [
|
|
16484
|
+
m[0] * v[0] + m[1] * v[1] + m[2] * v[2],
|
|
16485
|
+
m[3] * v[0] + m[4] * v[1] + m[5] * v[2],
|
|
16486
|
+
m[6] * v[0] + m[7] * v[1] + m[8] * v[2]
|
|
16487
|
+
];
|
|
16488
|
+
}
|
|
16489
|
+
function matT(m) {
|
|
16490
|
+
return [m[0], m[3], m[6], m[1], m[4], m[7], m[2], m[5], m[8]];
|
|
16491
|
+
}
|
|
16492
|
+
function rotY(a) {
|
|
16493
|
+
const c2 = Math.cos(a), s = Math.sin(a);
|
|
16494
|
+
return [c2, 0, s, 0, 1, 0, -s, 0, c2];
|
|
16495
|
+
}
|
|
16496
|
+
function rotAxis(axis, angle) {
|
|
16497
|
+
const [x, y, z] = [axis[0], axis[1], axis[2]];
|
|
16498
|
+
const c2 = Math.cos(angle), s = Math.sin(angle), t = 1 - c2;
|
|
16499
|
+
return [
|
|
16500
|
+
t * x * x + c2,
|
|
16501
|
+
t * x * y - s * z,
|
|
16502
|
+
t * x * z + s * y,
|
|
16503
|
+
t * x * y + s * z,
|
|
16504
|
+
t * y * y + c2,
|
|
16505
|
+
t * y * z - s * x,
|
|
16506
|
+
t * x * z - s * y,
|
|
16507
|
+
t * y * z + s * x,
|
|
16508
|
+
t * z * z + c2
|
|
16509
|
+
];
|
|
16510
|
+
}
|
|
16511
|
+
function quatFromMat(m) {
|
|
16512
|
+
const tr = m[0] + m[4] + m[8];
|
|
16513
|
+
let x, y, z, w;
|
|
16514
|
+
if (tr > 0) {
|
|
16515
|
+
const s = Math.sqrt(tr + 1) * 2;
|
|
16516
|
+
w = s / 4;
|
|
16517
|
+
x = (m[7] - m[5]) / s;
|
|
16518
|
+
y = (m[2] - m[6]) / s;
|
|
16519
|
+
z = (m[3] - m[1]) / s;
|
|
16520
|
+
} else if (m[0] > m[4] && m[0] > m[8]) {
|
|
16521
|
+
const s = Math.sqrt(1 + m[0] - m[4] - m[8]) * 2;
|
|
16522
|
+
w = (m[7] - m[5]) / s;
|
|
16523
|
+
x = s / 4;
|
|
16524
|
+
y = (m[1] + m[3]) / s;
|
|
16525
|
+
z = (m[2] + m[6]) / s;
|
|
16526
|
+
} else if (m[4] > m[8]) {
|
|
16527
|
+
const s = Math.sqrt(1 + m[4] - m[0] - m[8]) * 2;
|
|
16528
|
+
w = (m[2] - m[6]) / s;
|
|
16529
|
+
x = (m[1] + m[3]) / s;
|
|
16530
|
+
y = s / 4;
|
|
16531
|
+
z = (m[5] + m[7]) / s;
|
|
16532
|
+
} else {
|
|
16533
|
+
const s = Math.sqrt(1 + m[8] - m[0] - m[4]) * 2;
|
|
16534
|
+
w = (m[3] - m[1]) / s;
|
|
16535
|
+
x = (m[2] + m[6]) / s;
|
|
16536
|
+
y = (m[5] + m[7]) / s;
|
|
16537
|
+
z = s / 4;
|
|
16538
|
+
}
|
|
16539
|
+
const n = Math.hypot(x, y, z, w);
|
|
16540
|
+
return [x / n, y / n, z / n, w / n];
|
|
16541
|
+
}
|
|
16542
|
+
function matFromQuat(q) {
|
|
16543
|
+
const [x, y, z, w] = [q[0], q[1], q[2], q[3]];
|
|
16544
|
+
const n = x * x + y * y + z * z + w * w || 1;
|
|
16545
|
+
const s = 2 / n;
|
|
16546
|
+
const xx = x * x * s, yy = y * y * s, zz = z * z * s;
|
|
16547
|
+
const xy = x * y * s, xz = x * z * s, yz = y * z * s;
|
|
16548
|
+
const wx = w * x * s, wy = w * y * s, wz = w * z * s;
|
|
16549
|
+
return [
|
|
16550
|
+
1 - yy - zz,
|
|
16551
|
+
xy - wz,
|
|
16552
|
+
xz + wy,
|
|
16553
|
+
xy + wz,
|
|
16554
|
+
1 - xx - zz,
|
|
16555
|
+
yz - wx,
|
|
16556
|
+
xz - wy,
|
|
16557
|
+
yz + wx,
|
|
16558
|
+
1 - xx - yy
|
|
16559
|
+
];
|
|
16560
|
+
}
|
|
16561
|
+
function quatMul(a, b) {
|
|
16562
|
+
const [ax, ay, az, aw] = [a[0], a[1], a[2], a[3]];
|
|
16563
|
+
const [bx, by, bz, bw] = [b[0], b[1], b[2], b[3]];
|
|
16564
|
+
return [
|
|
16565
|
+
aw * bx + ax * bw + ay * bz - az * by,
|
|
16566
|
+
aw * by - ax * bz + ay * bw + az * bx,
|
|
16567
|
+
aw * bz + ax * by - ay * bx + az * bw,
|
|
16568
|
+
aw * bw - ax * bx - ay * by - az * bz
|
|
16569
|
+
];
|
|
16570
|
+
}
|
|
16571
|
+
var mean = (a) => {
|
|
16572
|
+
let s = 0;
|
|
16573
|
+
for (let i = 0; i < a.length; i++) s += a[i];
|
|
16574
|
+
return a.length ? s / a.length : NaN;
|
|
16575
|
+
};
|
|
16576
|
+
function percentile(values, q) {
|
|
16577
|
+
const a = Array.from(values).sort((x, y) => x - y);
|
|
16578
|
+
if (a.length === 0) return NaN;
|
|
16579
|
+
const pos = (a.length - 1) * q / 100;
|
|
16580
|
+
const lo = Math.floor(pos), hi = Math.ceil(pos);
|
|
16581
|
+
if (lo === hi) return a[lo];
|
|
16582
|
+
return a[lo] * (hi - pos) + a[hi] * (pos - lo);
|
|
16583
|
+
}
|
|
16584
|
+
var median2 = (a) => percentile(a, 50);
|
|
16585
|
+
function std(a) {
|
|
16586
|
+
const m = mean(a);
|
|
16587
|
+
let s = 0;
|
|
16588
|
+
for (let i = 0; i < a.length; i++) s += (a[i] - m) ** 2;
|
|
16589
|
+
return Math.sqrt(a.length ? s / a.length : NaN);
|
|
16590
|
+
}
|
|
16591
|
+
function unwrap(a) {
|
|
16592
|
+
const out = [a[0] ?? 0];
|
|
16593
|
+
for (let i = 1; i < a.length; i++) {
|
|
16594
|
+
let d = a[i] - a[i - 1];
|
|
16595
|
+
d -= 2 * Math.PI * Math.round(d / (2 * Math.PI));
|
|
16596
|
+
out.push(out[i - 1] + d);
|
|
16597
|
+
}
|
|
16598
|
+
return out;
|
|
16599
|
+
}
|
|
16600
|
+
function lstsqLinear(t, y) {
|
|
16601
|
+
const n = t.length;
|
|
16602
|
+
const mt = mean(t), my = mean(y);
|
|
16603
|
+
let sxx = 0, sxy = 0;
|
|
16604
|
+
for (let i = 0; i < n; i++) {
|
|
16605
|
+
sxx += (t[i] - mt) ** 2;
|
|
16606
|
+
sxy += (t[i] - mt) * (y[i] - my);
|
|
16607
|
+
}
|
|
16608
|
+
const slope = sxx > 0 ? sxy / sxx : 0;
|
|
16609
|
+
return { slope, intercept: my - slope * mt };
|
|
16610
|
+
}
|
|
16611
|
+
function loadTake(buf) {
|
|
16612
|
+
const npz = readNpz(buf);
|
|
16613
|
+
const posed = floats(npz, "posed_joints");
|
|
16614
|
+
const rootRaw = floats(npz, "root_positions");
|
|
16615
|
+
const lm = floats(npz, "local_rot_mats");
|
|
16616
|
+
const fcRaw = floats(npz, "foot_contacts");
|
|
16617
|
+
const F = posed.shape[0];
|
|
16618
|
+
if (lm.shape[1] !== J) throw new Error(`take has ${lm.shape[1]} joints, expected ${J}`);
|
|
16619
|
+
const P = [];
|
|
16620
|
+
const root = [];
|
|
16621
|
+
const localMats = [];
|
|
16622
|
+
const quat = [];
|
|
16623
|
+
const heading = [];
|
|
16624
|
+
const fc = [];
|
|
16625
|
+
for (let f = 0; f < F; f++) {
|
|
16626
|
+
const pf = [];
|
|
16627
|
+
for (let j = 0; j < J; j++) {
|
|
16628
|
+
const o = (f * J + j) * 3;
|
|
16629
|
+
pf.push([posed.a[o], posed.a[o + 1], posed.a[o + 2]]);
|
|
16630
|
+
}
|
|
16631
|
+
P.push(pf);
|
|
16632
|
+
root.push([rootRaw.a[f * 3], rootRaw.a[f * 3 + 1], rootRaw.a[f * 3 + 2]]);
|
|
16633
|
+
const mf = [];
|
|
16634
|
+
const qf = [];
|
|
16635
|
+
for (let j = 0; j < J; j++) {
|
|
16636
|
+
const o = (f * J + j) * 9;
|
|
16637
|
+
const m = Array.from(lm.a.subarray(o, o + 9));
|
|
16638
|
+
mf.push(m);
|
|
16639
|
+
qf.push(quatFromMat(m));
|
|
16640
|
+
}
|
|
16641
|
+
localMats.push(mf);
|
|
16642
|
+
quat.push(qf);
|
|
16643
|
+
const m0 = mf[0];
|
|
16644
|
+
heading.push(Math.atan2(m0[2], m0[8]));
|
|
16645
|
+
fc.push([fcRaw.a[f * 4] !== 0, fcRaw.a[f * 4 + 1] !== 0, fcRaw.a[f * 4 + 2] !== 0, fcRaw.a[f * 4 + 3] !== 0]);
|
|
16646
|
+
}
|
|
16647
|
+
for (let j = 0; j < J; j++) {
|
|
16648
|
+
for (let f = 1; f < F; f++) {
|
|
16649
|
+
const a = quat[f][j], b = quat[f - 1][j];
|
|
16650
|
+
if (a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3] < 0) {
|
|
16651
|
+
quat[f][j] = [-a[0], -a[1], -a[2], -a[3]];
|
|
16652
|
+
}
|
|
16653
|
+
}
|
|
16654
|
+
}
|
|
16655
|
+
let gm = null;
|
|
16656
|
+
if (npz.global_rot_mats) {
|
|
16657
|
+
const g = floats(npz, "global_rot_mats");
|
|
16658
|
+
gm = [];
|
|
16659
|
+
for (let f = 0; f < F; f++) {
|
|
16660
|
+
const gf = [];
|
|
16661
|
+
for (let j = 0; j < J; j++) {
|
|
16662
|
+
const o = (f * J + j) * 9;
|
|
16663
|
+
gf.push(Array.from(g.a.subarray(o, o + 9)));
|
|
16664
|
+
}
|
|
16665
|
+
gm.push(gf);
|
|
16666
|
+
}
|
|
16667
|
+
}
|
|
16668
|
+
const fpsMember = npz.fps;
|
|
16669
|
+
const fps = fpsMember && typeof fpsMember.data !== "string" ? Number(fpsMember.data[0]) : 20;
|
|
16670
|
+
const text = typeof npz.text?.data === "string" ? npz.text.data : "";
|
|
16671
|
+
return { F, fps, text, P, root, quat, heading, fc, gm, localMats };
|
|
16672
|
+
}
|
|
16673
|
+
function fkRots(localFrame) {
|
|
16674
|
+
const W = new Array(J);
|
|
16675
|
+
for (let j = 0; j < J; j++) {
|
|
16676
|
+
const p = PARENTS[j];
|
|
16677
|
+
W[j] = p < 0 ? localFrame[j] : matMul(W[p], localFrame[j]);
|
|
16678
|
+
}
|
|
16679
|
+
return W;
|
|
16680
|
+
}
|
|
16681
|
+
var I_HIPS = 0;
|
|
16682
|
+
function charFrame(P, heading) {
|
|
16683
|
+
const out = [];
|
|
16684
|
+
for (let f = 0; f < P.length; f++) {
|
|
16685
|
+
const rot = rotY(-heading[f]);
|
|
16686
|
+
const hx = P[f][I_HIPS][0], hz = P[f][I_HIPS][2];
|
|
16687
|
+
const cf = [];
|
|
16688
|
+
for (let j = 0; j < J; j++) {
|
|
16689
|
+
cf.push(matVec(rot, [P[f][j][0] - hx, P[f][j][1], P[f][j][2] - hz]));
|
|
16690
|
+
}
|
|
16691
|
+
out.push(cf);
|
|
16692
|
+
}
|
|
16693
|
+
return out;
|
|
16694
|
+
}
|
|
16695
|
+
function poseDist(quat, a, b) {
|
|
16696
|
+
let s = 0;
|
|
16697
|
+
for (let j = 0; j < J; j++) {
|
|
16698
|
+
const qa = quat[a][j], qb = quat[b][j];
|
|
16699
|
+
const d = Math.min(1, Math.abs(qa[0] * qb[0] + qa[1] * qb[1] + qa[2] * qb[2] + qa[3] * qb[3]));
|
|
16700
|
+
s += 2 * Math.acos(d);
|
|
16701
|
+
}
|
|
16702
|
+
return s / J;
|
|
16703
|
+
}
|
|
16704
|
+
|
|
16705
|
+
// src/lib/motion/normalize.ts
|
|
16706
|
+
var ARM_ROOTS = [ji("RightArm"), ji("LeftArm")];
|
|
16707
|
+
var I_RARM = ji("RightArm");
|
|
16708
|
+
var I_LARM = ji("LeftArm");
|
|
16709
|
+
var NECK_HEAD = [ji("Neck"), ji("Head")];
|
|
16710
|
+
var SPINE_IDX = ["Spine", "Spine1", "Spine2", "Spine3"].map(ji);
|
|
16711
|
+
var I_HIPS2 = ji("Hips");
|
|
16712
|
+
var I_RH = ji("RightHand");
|
|
16713
|
+
var I_LH = ji("LeftHand");
|
|
16714
|
+
var I_HEAD = ji("Head");
|
|
16715
|
+
var deg = Math.PI / 180;
|
|
16716
|
+
function deroot(quatCyc, rootCyc, fps, refHeading, targetHeading = 0) {
|
|
16717
|
+
const F = rootCyc.length;
|
|
16718
|
+
const ts = [], xs = [], zs = [];
|
|
16719
|
+
for (let f = 0; f < F; f++) {
|
|
16720
|
+
ts.push(f / fps);
|
|
16721
|
+
xs.push(rootCyc[f][0]);
|
|
16722
|
+
zs.push(rootCyc[f][2]);
|
|
16723
|
+
}
|
|
16724
|
+
const fx = lstsqLinear(ts, xs), fz = lstsqLinear(ts, zs);
|
|
16725
|
+
for (let f = 0; f < F; f++) {
|
|
16726
|
+
rootCyc[f][0] -= fx.intercept + fx.slope * ts[f];
|
|
16727
|
+
rootCyc[f][2] -= fz.intercept + fz.slope * ts[f];
|
|
16728
|
+
}
|
|
16729
|
+
const theta = targetHeading - refHeading;
|
|
16730
|
+
const rotM = rotY(theta);
|
|
16731
|
+
const half = theta / 2;
|
|
16732
|
+
const rotQ = [0, Math.sin(half), 0, Math.cos(half)];
|
|
16733
|
+
for (let f = 0; f < F; f++) {
|
|
16734
|
+
const r = matVec(rotM, rootCyc[f]);
|
|
16735
|
+
rootCyc[f][0] = r[0];
|
|
16736
|
+
rootCyc[f][2] = r[2];
|
|
16737
|
+
quatCyc[f][0] = quatMul(rotQ, quatCyc[f][0]);
|
|
16738
|
+
}
|
|
16739
|
+
const v = matVec(rotM, [fx.slope, 0, fz.slope]);
|
|
16740
|
+
const speed = Math.hypot(v[0], v[2]);
|
|
16741
|
+
return { speed, dir: speed > 1e-6 ? [v[0] / speed, v[2] / speed] : [0, 0] };
|
|
16742
|
+
}
|
|
16743
|
+
function levelArms(qframe, Pf, pitchDeg) {
|
|
16744
|
+
const ax = [
|
|
16745
|
+
Pf[I_LARM][0] - Pf[I_RARM][0],
|
|
16746
|
+
Pf[I_LARM][1] - Pf[I_RARM][1],
|
|
16747
|
+
Pf[I_LARM][2] - Pf[I_RARM][2]
|
|
16748
|
+
];
|
|
16749
|
+
const n = Math.hypot(...ax) + 1e-9;
|
|
16750
|
+
const Rax = rotAxis([ax[0] / n, ax[1] / n, ax[2] / n], pitchDeg * deg);
|
|
16751
|
+
const local = qframe.map(matFromQuat);
|
|
16752
|
+
const W = fkRots(local);
|
|
16753
|
+
const out = qframe.slice();
|
|
16754
|
+
for (const j of ARM_ROOTS) {
|
|
16755
|
+
const Wp = W[PARENTS[j]];
|
|
16756
|
+
out[j] = quatFromMat(matMul(matT(Wp), matMul(Rax, matMul(Wp, local[j]))));
|
|
16757
|
+
}
|
|
16758
|
+
return out;
|
|
16759
|
+
}
|
|
16760
|
+
function levelHead(qframe, Pf, excessDeg) {
|
|
16761
|
+
const ax = [
|
|
16762
|
+
Pf[I_LARM][0] - Pf[I_RARM][0],
|
|
16763
|
+
Pf[I_LARM][1] - Pf[I_RARM][1],
|
|
16764
|
+
Pf[I_LARM][2] - Pf[I_RARM][2]
|
|
16765
|
+
];
|
|
16766
|
+
const n = Math.hypot(...ax) + 1e-9;
|
|
16767
|
+
const Rax = rotAxis([ax[0] / n, ax[1] / n, ax[2] / n], -excessDeg / 2 * deg);
|
|
16768
|
+
const local = qframe.map(matFromQuat);
|
|
16769
|
+
const W = fkRots(local);
|
|
16770
|
+
const out = qframe.slice();
|
|
16771
|
+
for (const j of NECK_HEAD) {
|
|
16772
|
+
const Wp = W[PARENTS[j]];
|
|
16773
|
+
out[j] = quatFromMat(matMul(matT(Wp), matMul(Rax, matMul(Wp, local[j]))));
|
|
16774
|
+
local[j] = matFromQuat(out[j]);
|
|
16775
|
+
W[j] = matMul(Wp, local[j]);
|
|
16776
|
+
}
|
|
16777
|
+
return out;
|
|
16778
|
+
}
|
|
16779
|
+
function headPitchFrames(localMats, s, e) {
|
|
16780
|
+
const vals = [];
|
|
16781
|
+
for (let f = s; f < e; f++) {
|
|
16782
|
+
const W = fkRots(localMats[f]);
|
|
16783
|
+
const fzY = W[NECK_HEAD[1]][5];
|
|
16784
|
+
vals.push(Math.asin(Math.max(-1, Math.min(1, -fzY))) / deg);
|
|
16785
|
+
}
|
|
16786
|
+
return mean(vals);
|
|
16787
|
+
}
|
|
16788
|
+
function pitched(q, theta) {
|
|
16789
|
+
const out = q.slice();
|
|
16790
|
+
const half = theta / SPINE_IDX.length / 2;
|
|
16791
|
+
const step = [Math.sin(half), 0, 0, Math.cos(half)];
|
|
16792
|
+
for (const si of SPINE_IDX) out[si] = quatMul(step, out[si]);
|
|
16793
|
+
return out;
|
|
16794
|
+
}
|
|
16795
|
+
function yawed(q, theta) {
|
|
16796
|
+
const out = q.slice();
|
|
16797
|
+
const half = theta / SPINE_IDX.length / 2;
|
|
16798
|
+
const step = [0, Math.sin(half), 0, Math.cos(half)];
|
|
16799
|
+
for (const si of SPINE_IDX) out[si] = quatMul(step, out[si]);
|
|
16800
|
+
return out;
|
|
16801
|
+
}
|
|
16802
|
+
function gripLineChar(P, heading) {
|
|
16803
|
+
const yaws = [], pitches = [];
|
|
16804
|
+
for (let f = 0; f < P.length; f++) {
|
|
16805
|
+
const rot = rotY(-heading[f]);
|
|
16806
|
+
const rel = (j) => matVec(rot, [
|
|
16807
|
+
P[f][j][0] - P[f][I_HIPS2][0],
|
|
16808
|
+
P[f][j][1] - P[f][I_HIPS2][1],
|
|
16809
|
+
P[f][j][2] - P[f][I_HIPS2][2]
|
|
16810
|
+
]);
|
|
16811
|
+
const rh = rel(I_RH), lh = rel(I_LH);
|
|
16812
|
+
const v = [lh[0] - rh[0], lh[1] - rh[1], lh[2] - rh[2]];
|
|
16813
|
+
yaws.push(Math.atan2(v[0], Math.max(v[2], 1e-6)) / deg);
|
|
16814
|
+
pitches.push(Math.atan2(v[1], Math.hypot(v[0], v[2]) + 1e-9) / deg);
|
|
16815
|
+
}
|
|
16816
|
+
return { yaws, pitches };
|
|
16817
|
+
}
|
|
16818
|
+
|
|
16819
|
+
// src/lib/motion/verify.ts
|
|
16820
|
+
var I_HIPS3 = ji("Hips");
|
|
16821
|
+
var I_HEAD2 = ji("Head");
|
|
16822
|
+
var I_RSH = ji("RightShoulder");
|
|
16823
|
+
var I_LSH = ji("LeftShoulder");
|
|
16824
|
+
var I_RF = ji("RightFoot");
|
|
16825
|
+
var I_RT = ji("RightToeBase");
|
|
16826
|
+
var I_LF = ji("LeftFoot");
|
|
16827
|
+
var I_LT = ji("LeftToeBase");
|
|
16828
|
+
var I_NECK = ji("Neck");
|
|
16829
|
+
var I_RH2 = ji("RightHand");
|
|
16830
|
+
var I_LH2 = ji("LeftHand");
|
|
16831
|
+
var I_CHEST = ji("Spine2");
|
|
16832
|
+
var RAMP = 50;
|
|
16833
|
+
var TAIL = 10;
|
|
16834
|
+
var D = Math.sqrt(0.5);
|
|
16835
|
+
var EXPECT_DIR = {
|
|
16836
|
+
// ARDY frame: +z forward, +x LEFT of the character
|
|
16837
|
+
forward: [0, 1],
|
|
16838
|
+
back: [0, -1],
|
|
16839
|
+
"strafe-left": [1, 0],
|
|
16840
|
+
"strafe-right": [-1, 0],
|
|
16841
|
+
fl: [D, D],
|
|
16842
|
+
fr: [-D, D],
|
|
16843
|
+
bl: [D, -D],
|
|
16844
|
+
br: [-D, -D]
|
|
16845
|
+
};
|
|
16846
|
+
function classify(stemRaw) {
|
|
16847
|
+
const stem = stemRaw.replace(/-s\d+$/, "");
|
|
16848
|
+
if (stem.includes("jump") || stem.includes("ollie") || stem.includes("trick")) {
|
|
16849
|
+
return { kind: "oneshot", expect: null };
|
|
16850
|
+
}
|
|
16851
|
+
for (const [key, dxz] of Object.entries(EXPECT_DIR)) {
|
|
16852
|
+
if (stem.endsWith(`-${key}`) || stem.includes(`-run-${key}`)) return { kind: "loco", expect: dxz };
|
|
16853
|
+
}
|
|
16854
|
+
if (stem.includes("-idle") || stem.includes("stance")) return { kind: "idle", expect: null };
|
|
16855
|
+
if (stem.includes("push")) return { kind: "travel", expect: null };
|
|
16856
|
+
return { kind: "freeform", expect: null };
|
|
16857
|
+
}
|
|
16858
|
+
function mSlip(t, w) {
|
|
16859
|
+
const { P, fc, fps } = t;
|
|
16860
|
+
const vals = [];
|
|
16861
|
+
for (const [cA, cB, iF, iT] of [[0, 1, I_LF, I_LT], [2, 3, I_RF, I_RT]]) {
|
|
16862
|
+
const s = [];
|
|
16863
|
+
for (let f = w[0]; f < w[1] - 1; f++) {
|
|
16864
|
+
if (!(fc[f][cA] || fc[f][cB])) continue;
|
|
16865
|
+
const sp = (j) => Math.hypot(P[f + 1][j][0] - P[f][j][0], P[f + 1][j][2] - P[f][j][2]) * fps;
|
|
16866
|
+
s.push(Math.min(sp(iF), sp(iT)));
|
|
16867
|
+
}
|
|
16868
|
+
if (s.length < 3) continue;
|
|
16869
|
+
vals.push(mean(s));
|
|
16870
|
+
}
|
|
16871
|
+
return vals.length ? mean(vals) * 100 : null;
|
|
16872
|
+
}
|
|
16873
|
+
function mTravel(t, w) {
|
|
16874
|
+
const ts = [], xs = [], zs = [];
|
|
16875
|
+
for (let f = w[0]; f < w[1]; f++) {
|
|
16876
|
+
ts.push(f / t.fps);
|
|
16877
|
+
xs.push(t.root[f][0]);
|
|
16878
|
+
zs.push(t.root[f][2]);
|
|
16879
|
+
}
|
|
16880
|
+
const vx = lstsqLinear(ts, xs).slope;
|
|
16881
|
+
const vz = lstsqLinear(ts, zs).slope;
|
|
16882
|
+
const v = matVec(rotY(-mean(t.heading.slice(w[0], w[1]))), [vx, 0, vz]);
|
|
16883
|
+
const speed = Math.hypot(v[0], v[2]);
|
|
16884
|
+
return { speed, dir: speed > 1e-6 ? [v[0] / speed, v[2] / speed] : null };
|
|
16885
|
+
}
|
|
16886
|
+
var mHeadingStd = (t, w) => std(unwrap(t.heading.slice(w[0], w[1]))) * 180 / Math.PI;
|
|
16887
|
+
function mBlade(C, w) {
|
|
16888
|
+
let ss = 0, cs = 0, n = 0;
|
|
16889
|
+
for (let f = w[0]; f < w[1]; f++) {
|
|
16890
|
+
const shx = C[f][I_RSH][0] - C[f][I_LSH][0];
|
|
16891
|
+
const shz = C[f][I_RSH][2] - C[f][I_LSH][2];
|
|
16892
|
+
const ang = Math.atan2(shz, -shx);
|
|
16893
|
+
ss += Math.sin(ang);
|
|
16894
|
+
cs += Math.cos(ang);
|
|
16895
|
+
n++;
|
|
16896
|
+
}
|
|
16897
|
+
return Math.abs(Math.atan2(ss / n, cs / n)) * (180 / Math.PI);
|
|
16898
|
+
}
|
|
16899
|
+
function mSpeedCv(t, w) {
|
|
16900
|
+
const n = Math.max(2, Math.floor(t.fps / 2));
|
|
16901
|
+
const sp = [];
|
|
16902
|
+
for (let s = w[0]; s < w[1] - n; s += n) {
|
|
16903
|
+
sp.push(Math.hypot(t.root[s + n][0] - t.root[s][0], t.root[s + n][2] - t.root[s][2]) / (n / t.fps));
|
|
16904
|
+
}
|
|
16905
|
+
return sp.length >= 3 ? std(sp) / (mean(sp) + 1e-6) : null;
|
|
16906
|
+
}
|
|
16907
|
+
function mRoughness(t, w) {
|
|
16908
|
+
const F = w[1] - w[0];
|
|
16909
|
+
if (F < 5) return null;
|
|
16910
|
+
let accSq = 0, velSq = 0, n = 0;
|
|
16911
|
+
for (let f = w[0] + 1; f < w[1] - 1; f++) {
|
|
16912
|
+
for (let j = 0; j < t.P[f].length; j++) {
|
|
16913
|
+
for (let k = 0; k < 3; k++) {
|
|
16914
|
+
const a = t.P[f + 1][j][k] - 2 * t.P[f][j][k] + t.P[f - 1][j][k];
|
|
16915
|
+
const v = (t.P[f + 1][j][k] - t.P[f - 1][j][k]) / 2;
|
|
16916
|
+
accSq += a * a;
|
|
16917
|
+
velSq += v * v;
|
|
16918
|
+
n++;
|
|
16919
|
+
}
|
|
16920
|
+
}
|
|
16921
|
+
}
|
|
16922
|
+
const velRms = Math.max(Math.sqrt(velSq / n), 6e-3);
|
|
16923
|
+
return Math.sqrt(accSq / n) / velRms;
|
|
16924
|
+
}
|
|
16925
|
+
function mHeadyaw(t, w) {
|
|
16926
|
+
if (!t.gm) return null;
|
|
16927
|
+
let ss = 0, cs = 0, n = 0;
|
|
16928
|
+
for (let f = w[0]; f < w[1]; f++) {
|
|
16929
|
+
const yawOf = (idx) => {
|
|
16930
|
+
const m = t.gm[f][idx];
|
|
16931
|
+
return Math.atan2(m[2], m[8]) * 180 / Math.PI;
|
|
16932
|
+
};
|
|
16933
|
+
const dDeg = ((yawOf(I_HEAD2) - yawOf(I_HIPS3) + 180) % 360 + 360) % 360 - 180;
|
|
16934
|
+
const d = dDeg * Math.PI / 180;
|
|
16935
|
+
ss += Math.sin(d);
|
|
16936
|
+
cs += Math.cos(d);
|
|
16937
|
+
n++;
|
|
16938
|
+
}
|
|
16939
|
+
return Math.abs(Math.atan2(ss / n, cs / n) * 180 / Math.PI);
|
|
16940
|
+
}
|
|
16941
|
+
function mHeadpitch(t, w) {
|
|
16942
|
+
if (!t.gm) return null;
|
|
16943
|
+
let s = 0, n = 0;
|
|
16944
|
+
for (let f = w[0]; f < w[1]; f++) {
|
|
16945
|
+
s += t.gm[f][I_HEAD2][5];
|
|
16946
|
+
n++;
|
|
16947
|
+
}
|
|
16948
|
+
const v = Math.max(-1, Math.min(1, -(s / n)));
|
|
16949
|
+
return Math.asin(v) * 180 / Math.PI;
|
|
16950
|
+
}
|
|
16951
|
+
function mGround(t, w) {
|
|
16952
|
+
const feet = [I_LF, I_LT, I_RF, I_RT];
|
|
16953
|
+
let minAll = Infinity;
|
|
16954
|
+
const minOn = [];
|
|
16955
|
+
for (let f = w[0]; f < w[1]; f++) {
|
|
16956
|
+
let mn = Infinity;
|
|
16957
|
+
for (const j of feet) mn = Math.min(mn, t.P[f][j][1]);
|
|
16958
|
+
minAll = Math.min(minAll, mn);
|
|
16959
|
+
if (t.fc[f].some(Boolean)) minOn.push(mn);
|
|
16960
|
+
}
|
|
16961
|
+
return {
|
|
16962
|
+
pen: Math.max(0, -minAll) * 100,
|
|
16963
|
+
float: minOn.length >= 3 ? median2(minOn) * 100 : null
|
|
16964
|
+
};
|
|
16965
|
+
}
|
|
16966
|
+
function mHipsRatio(t, w) {
|
|
16967
|
+
const ys = [];
|
|
16968
|
+
for (let f = w[0]; f < w[1]; f++) if (t.fc[f].some(Boolean)) ys.push(t.P[f][I_HIPS3][1]);
|
|
16969
|
+
return ys.length >= 3 ? median2(ys) / HIPS_REST_Y : null;
|
|
16970
|
+
}
|
|
16971
|
+
function mSeam(t, w, kind) {
|
|
16972
|
+
if (kind === "loco" || kind === "travel") {
|
|
16973
|
+
const ls = [];
|
|
16974
|
+
for (let f = w[0] + 1; f < w[1]; f++) {
|
|
16975
|
+
const on = (i) => t.fc[i][0] || t.fc[i][1];
|
|
16976
|
+
if (on(f) && !on(f - 1)) ls.push(f);
|
|
16977
|
+
}
|
|
16978
|
+
let best2 = null;
|
|
16979
|
+
for (let i = 0; i < ls.length - 1; i++) {
|
|
16980
|
+
const s = ls[i];
|
|
16981
|
+
for (const e of [ls[i + 1] - 1, ls[i + 1], ls[i + 1] + 1]) {
|
|
16982
|
+
if (e <= s + 6 || e >= w[1]) continue;
|
|
16983
|
+
const d = poseDist(t.quat, s, e);
|
|
16984
|
+
if (best2 === null || d < best2) best2 = d;
|
|
16985
|
+
}
|
|
16986
|
+
}
|
|
16987
|
+
return best2 === null ? null : best2 * 180 / Math.PI;
|
|
16988
|
+
}
|
|
16989
|
+
let best = null;
|
|
16990
|
+
const [lo, hi] = w;
|
|
16991
|
+
for (let s = lo; s < hi - 30; s += 2) {
|
|
16992
|
+
for (let L = Math.floor(t.fps); L < Math.floor(t.fps * 3); L += 4) {
|
|
16993
|
+
const e = s + L;
|
|
16994
|
+
if (e >= hi) continue;
|
|
16995
|
+
const d = poseDist(t.quat, s, e);
|
|
16996
|
+
if (best === null || d < best) best = d;
|
|
16997
|
+
}
|
|
16998
|
+
}
|
|
16999
|
+
return best === null ? null : best * 180 / Math.PI;
|
|
17000
|
+
}
|
|
17001
|
+
function mOneshot(t, w) {
|
|
17002
|
+
let maxY = -Infinity;
|
|
17003
|
+
const early = [];
|
|
17004
|
+
let air = 0;
|
|
17005
|
+
for (let f = w[0]; f < w[1]; f++) {
|
|
17006
|
+
maxY = Math.max(maxY, t.root[f][1]);
|
|
17007
|
+
if (f < w[0] + 10) early.push(t.root[f][1]);
|
|
17008
|
+
if (!t.fc[f].some(Boolean)) air++;
|
|
17009
|
+
}
|
|
17010
|
+
return { rise: maxY - median2(early), air };
|
|
17011
|
+
}
|
|
17012
|
+
function mAimhold(C, w) {
|
|
17013
|
+
const hunchF = [], dyF = [], gapF = [];
|
|
17014
|
+
const yawSigned = [], pitchF = [], archF = [];
|
|
17015
|
+
const fwdSrc = [];
|
|
17016
|
+
const handFwd = [], rhClear = [];
|
|
17017
|
+
for (let f = w[0]; f < w[1]; f++) {
|
|
17018
|
+
const up = [
|
|
17019
|
+
C[f][I_HEAD2][0] - C[f][I_HIPS3][0],
|
|
17020
|
+
C[f][I_HEAD2][1] - C[f][I_HIPS3][1],
|
|
17021
|
+
C[f][I_HEAD2][2] - C[f][I_HIPS3][2]
|
|
17022
|
+
];
|
|
17023
|
+
const upN = Math.hypot(...up) + 1e-9;
|
|
17024
|
+
hunchF.push(Math.acos(Math.max(-1, Math.min(1, up[1] / upN))) * 180 / Math.PI);
|
|
17025
|
+
const rh = C[f][I_RH2], lh = C[f][I_LH2];
|
|
17026
|
+
dyF.push(0.5 * (rh[1] + lh[1]) - C[f][I_HEAD2][1]);
|
|
17027
|
+
handFwd.push(0.5 * (rh[2] + lh[2]));
|
|
17028
|
+
const a = C[f][I_HIPS3], b = C[f][I_NECK], p = rh;
|
|
17029
|
+
const ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
|
17030
|
+
const tt = Math.max(0, Math.min(
|
|
17031
|
+
1,
|
|
17032
|
+
((p[0] - a[0]) * ab[0] + (p[1] - a[1]) * ab[1] + (p[2] - a[2]) * ab[2]) / (ab[0] ** 2 + ab[1] ** 2 + ab[2] ** 2 + 1e-9)
|
|
17033
|
+
));
|
|
17034
|
+
rhClear.push(Math.hypot(p[0] - (a[0] + tt * ab[0]), p[1] - (a[1] + tt * ab[1]), p[2] - (a[2] + tt * ab[2])));
|
|
17035
|
+
const v = [lh[0] - rh[0], lh[1] - rh[1], lh[2] - rh[2]];
|
|
17036
|
+
const L = Math.hypot(...v);
|
|
17037
|
+
gapF.push(L);
|
|
17038
|
+
fwdSrc.push({ v, L });
|
|
17039
|
+
yawSigned.push(Math.atan2(v[0], Math.max(v[2], 1e-6)) * 180 / Math.PI);
|
|
17040
|
+
pitchF.push(Math.atan2(v[1], Math.hypot(v[0], v[2]) + 1e-9) * 180 / Math.PI);
|
|
17041
|
+
const c2 = C[f][I_CHEST], h2 = C[f][I_HEAD2];
|
|
17042
|
+
const ab2 = [h2[0] - a[0], h2[1] - a[1], h2[2] - a[2]];
|
|
17043
|
+
const t2 = ((c2[0] - a[0]) * ab2[0] + (c2[1] - a[1]) * ab2[1] + (c2[2] - a[2]) * ab2[2]) / (ab2[0] ** 2 + ab2[1] ** 2 + ab2[2] ** 2 + 1e-9);
|
|
17044
|
+
archF.push(c2[2] - (a[2] + t2 * ab2[2]));
|
|
17045
|
+
}
|
|
17046
|
+
const yawMed = median2(yawSigned);
|
|
17047
|
+
const dev = yawSigned.map((y) => Math.abs(y - yawMed));
|
|
17048
|
+
const r = yawMed * Math.PI / 180;
|
|
17049
|
+
const axis = [Math.sin(r), 0, Math.cos(r)];
|
|
17050
|
+
const fwdF = fwdSrc.map(({ v, L }) => (v[0] * axis[0] + v[1] * axis[1] + v[2] * axis[2]) / (L + 1e-9));
|
|
17051
|
+
let run2 = 0, best = 0;
|
|
17052
|
+
for (let i = 0; i < fwdF.length; i++) {
|
|
17053
|
+
const ok = dev[i] < 15 && dyF[i] < -0.22 && gapF[i] > 0.18 && gapF[i] < 0.46 && fwdF[i] > 0.75 && hunchF[i] < 16;
|
|
17054
|
+
run2 = ok ? run2 + 1 : 0;
|
|
17055
|
+
best = Math.max(best, run2);
|
|
17056
|
+
}
|
|
17057
|
+
let holdBest = 0, holdBestScore = -Infinity;
|
|
17058
|
+
for (let i = 0; i < fwdF.length; i++) {
|
|
17059
|
+
const s = fwdF[i] - dev[i] / 45 - Math.abs(gapF[i] - 0.32);
|
|
17060
|
+
if (s > holdBestScore) {
|
|
17061
|
+
holdBestScore = s;
|
|
17062
|
+
holdBest = i;
|
|
17063
|
+
}
|
|
17064
|
+
}
|
|
17065
|
+
return {
|
|
17066
|
+
hunch_deg: percentile(hunchF, 90),
|
|
17067
|
+
hand_dy_m: mean(dyF),
|
|
17068
|
+
hand_fwd_m: mean(handFwd),
|
|
17069
|
+
rh_clear_m: mean(rhClear),
|
|
17070
|
+
hand_gap_m: percentile(gapF, 90),
|
|
17071
|
+
line_fwd: mean(fwdF),
|
|
17072
|
+
line_yaw_deg: percentile(dev, 90),
|
|
17073
|
+
line_yaw_med_deg: yawMed,
|
|
17074
|
+
line_pitch_deg: median2(pitchF),
|
|
17075
|
+
arch_m: median2(archF),
|
|
17076
|
+
hold_run: best,
|
|
17077
|
+
hold_best_f: holdBest + w[0]
|
|
17078
|
+
};
|
|
17079
|
+
}
|
|
17080
|
+
var DEFAULT_GATES = {
|
|
17081
|
+
pass: 70,
|
|
17082
|
+
warn: 50,
|
|
17083
|
+
common: {
|
|
17084
|
+
slip: { w: 3, good: 12, bad: 45 },
|
|
17085
|
+
rough: { w: 2, good: 0.4, bad: 0.8 },
|
|
17086
|
+
pen: { w: 1, good: 1.5, bad: 8 },
|
|
17087
|
+
float: { w: 1, good: 4, bad: 12 },
|
|
17088
|
+
headyaw: { w: 2, good: 15, bad: 45 }
|
|
17089
|
+
},
|
|
17090
|
+
aim: {
|
|
17091
|
+
hunch: { w: 3, good: 10, bad: 18 },
|
|
17092
|
+
handhigh: { w: 2, good: -0.32, bad: -0.22 },
|
|
17093
|
+
handfwd: { w: 2, good: 0.3, bad: 0.12, invert: true },
|
|
17094
|
+
handclear: { w: 2, good: 0.16, bad: 0.07, invert: true },
|
|
17095
|
+
twohand: { w: 3, good: 0.35, bad: 0.48 },
|
|
17096
|
+
lineFwd: { w: 3, good: 0.88, bad: 0.75, invert: true },
|
|
17097
|
+
lineYaw: { w: 3, good: 8, bad: 18 },
|
|
17098
|
+
lineMed: { w: 2, good: 45, bad: 65 },
|
|
17099
|
+
holdRun: { w: 3, good: 15, bad: 6, invert: true },
|
|
17100
|
+
linePitch: { w: 2, good: 8, bad: 22 },
|
|
17101
|
+
arch: { w: 1, good: -0.1, bad: -0.16, invert: true },
|
|
17102
|
+
headDown: { w: 2, good: 15, bad: 30 },
|
|
17103
|
+
tiptoe: { w: 1, good: 1.03, bad: 1.07 }
|
|
17104
|
+
},
|
|
17105
|
+
loco: {
|
|
17106
|
+
dir: { w: 3, good: 0.92, bad: 0.6, invert: true },
|
|
17107
|
+
headstd: { w: 1, good: 8, bad: 30 },
|
|
17108
|
+
blade: { w: 2, good: 28, bad: 60 },
|
|
17109
|
+
speedcv: { w: 1, good: 0.18, bad: 0.5 },
|
|
17110
|
+
seam: { w: 2, good: 6, bad: 20 },
|
|
17111
|
+
hips: { w: 1, good: 0.82, bad: 0.55, invert: true }
|
|
17112
|
+
},
|
|
17113
|
+
idle: {
|
|
17114
|
+
still: { w: 2, good: 0.06, bad: 0.35 },
|
|
17115
|
+
blade: { w: 1, good: 30, bad: 65 },
|
|
17116
|
+
seam: { w: 2, good: 6, bad: 20 },
|
|
17117
|
+
hips: { w: 2, good: 0.82, bad: 0.55, invert: true }
|
|
17118
|
+
},
|
|
17119
|
+
oneshot: {
|
|
17120
|
+
hips: { w: 1, good: 0.8, bad: 0.5, invert: true }
|
|
17121
|
+
},
|
|
17122
|
+
freeform: {
|
|
17123
|
+
seam: { w: 1, good: 8, bad: 25 }
|
|
17124
|
+
}
|
|
17125
|
+
};
|
|
17126
|
+
function soft(x, good, bad, invert = false) {
|
|
17127
|
+
if (x === null || x === void 0) return null;
|
|
17128
|
+
if (invert) {
|
|
17129
|
+
x = -x;
|
|
17130
|
+
const g = -good, b = -bad;
|
|
17131
|
+
good = g;
|
|
17132
|
+
bad = b;
|
|
17133
|
+
}
|
|
17134
|
+
return Math.max(0, Math.min(1, (bad - x) / (bad - good)));
|
|
17135
|
+
}
|
|
17136
|
+
var WHY = {
|
|
17137
|
+
slip: { fmt: (v) => `${v.toFixed(0)} cm/s`, why: "feet skate during stance" },
|
|
17138
|
+
rough: { fmt: (v) => v.toFixed(2), why: "jittery motion" },
|
|
17139
|
+
pen: { fmt: (v) => `${v.toFixed(1)} cm`, why: "feet clip the floor" },
|
|
17140
|
+
float: { fmt: (v) => `${v.toFixed(1)} cm`, why: "floats above the floor" },
|
|
17141
|
+
headyaw: { fmt: (v) => `${v.toFixed(0)}\xB0`, why: "head/body twisted \u2014 bladed stance" },
|
|
17142
|
+
hunch: { fmt: (v) => `${v.toFixed(0)}\xB0`, why: "hunched marksman crouch" },
|
|
17143
|
+
handhigh: { fmt: (v) => `${v.toFixed(2)} m`, why: "hands at face height (cheek weld)" },
|
|
17144
|
+
handfwd: { fmt: (v) => `${v.toFixed(2)} m`, why: "weapon held tight to the chest" },
|
|
17145
|
+
handclear: { fmt: (v) => `${v.toFixed(2)} m`, why: "trigger hand hugs the torso" },
|
|
17146
|
+
twohand: { fmt: (v) => `${v.toFixed(2)} m`, why: "support hand off the weapon (one-handed hold)" },
|
|
17147
|
+
lineFwd: { fmt: (v) => v.toFixed(2), why: "support hand off the take's own barrel axis" },
|
|
17148
|
+
lineYaw: { fmt: (v) => `${v.toFixed(0)}\xB0`, why: "grip line wanders \u2014 no stable barrel axis" },
|
|
17149
|
+
lineMed: { fmt: (v) => `${v.toFixed(0)}\xB0`, why: "median line beyond bladed-stance range (cross-chest carry)" },
|
|
17150
|
+
holdRun: { fmt: (v) => `${v.toFixed(0)}f`, why: "no sustained clean hold segment to pluck/loop" },
|
|
17151
|
+
linePitch: { fmt: (v) => `${v.toFixed(0)}\xB0`, why: "grip line slopes up/down \u2014 barrel not level" },
|
|
17152
|
+
arch: { fmt: (v) => `${v.toFixed(2)} m`, why: "leaning-back spine bow (hips forward, chest behind)" },
|
|
17153
|
+
headDown: { fmt: (v) => `${v.toFixed(0)}\xB0`, why: "head tilted down at the hold" },
|
|
17154
|
+
tiptoe: { fmt: (v) => v.toFixed(2), why: "stands taller than rest \u2014 short-legged rigs dangle/float" },
|
|
17155
|
+
dir: { fmt: (v) => v.toFixed(2), why: "doesn't travel the commanded direction" },
|
|
17156
|
+
headstd: { fmt: (v) => `${v.toFixed(0)}\xB0`, why: "heading wanders" },
|
|
17157
|
+
blade: { fmt: (v) => `${v.toFixed(0)}\xB0`, why: "bladed stance" },
|
|
17158
|
+
speedcv: { fmt: (v) => v.toFixed(2), why: "speed surges" },
|
|
17159
|
+
seam: { fmt: (v) => `${v.toFixed(1)}\xB0`, why: "no clean loop boundary" },
|
|
17160
|
+
hips: { fmt: (v) => v.toFixed(2), why: "crouched/collapsed" },
|
|
17161
|
+
still: { fmt: (v) => `${v.toFixed(2)} m/s`, why: "idle drifts" }
|
|
17162
|
+
};
|
|
17163
|
+
function score(kind, m, gates = DEFAULT_GATES) {
|
|
17164
|
+
const subs = {};
|
|
17165
|
+
const reasons = [];
|
|
17166
|
+
const add = (name, band, val, whyKey = name) => {
|
|
17167
|
+
if (!band) return;
|
|
17168
|
+
const s = soft(val ?? null, band.good, band.bad, band.invert);
|
|
17169
|
+
if (s === null) return;
|
|
17170
|
+
subs[name] = [s, band.w];
|
|
17171
|
+
const info = WHY[whyKey];
|
|
17172
|
+
if (s < 0.5 && info && val !== null && val !== void 0) {
|
|
17173
|
+
reasons.push(`${name} ${info.fmt(val)} (${info.why})`);
|
|
17174
|
+
}
|
|
17175
|
+
};
|
|
17176
|
+
const num = (k) => typeof m[k] === "number" && Number.isFinite(m[k]) ? m[k] : null;
|
|
17177
|
+
add("slip", gates.common.slip, num("slip_cms"));
|
|
17178
|
+
add("rough", gates.common.rough, num("roughness"));
|
|
17179
|
+
add("pen", gates.common.pen, num("pen_cm"));
|
|
17180
|
+
if (num("float_cm") !== null) add("float", gates.common.float, num("float_cm"));
|
|
17181
|
+
if (kind !== "oneshot" && kind !== "freeform" && !m.twist_ok) {
|
|
17182
|
+
add("headyaw", gates.common.headyaw, num("headyaw_deg"));
|
|
17183
|
+
}
|
|
17184
|
+
if (m.aim) {
|
|
17185
|
+
add("hunch", gates.aim.hunch, num("hunch_deg"));
|
|
17186
|
+
add("handhigh", gates.aim.handhigh, num("hand_dy_m"));
|
|
17187
|
+
add("handfwd", gates.aim.handfwd, num("hand_fwd_m"));
|
|
17188
|
+
add("handclear", gates.aim.handclear, num("rh_clear_m"));
|
|
17189
|
+
add("twohand", gates.aim.twohand, num("hand_gap_m"));
|
|
17190
|
+
add("lineFwd", gates.aim.lineFwd, num("line_fwd"));
|
|
17191
|
+
add("lineYaw", gates.aim.lineYaw, num("line_yaw_deg"));
|
|
17192
|
+
const lm = num("line_yaw_med_deg");
|
|
17193
|
+
add("lineMed", gates.aim.lineMed, lm === null ? null : Math.abs(lm));
|
|
17194
|
+
add("holdRun", gates.aim.holdRun, num("hold_run"));
|
|
17195
|
+
const lp = num("line_pitch_deg");
|
|
17196
|
+
add("linePitch", gates.aim.linePitch, lp === null ? null : Math.abs(lp));
|
|
17197
|
+
add("arch", gates.aim.arch, num("arch_m"));
|
|
17198
|
+
add("headDown", gates.aim.headDown, num("headpitch_deg"));
|
|
17199
|
+
add("tiptoe", gates.aim.tiptoe, num("hips_ratio"));
|
|
17200
|
+
}
|
|
17201
|
+
if (kind === "loco") {
|
|
17202
|
+
add("dir", gates.loco.dir, num("dir_dot"));
|
|
17203
|
+
add("headstd", gates.loco.headstd, num("heading_std"));
|
|
17204
|
+
add("blade", gates.loco.blade, num("blade_deg"));
|
|
17205
|
+
add("speedcv", gates.loco.speedcv, num("speed_cv"));
|
|
17206
|
+
add("seam", gates.loco.seam, num("seam_deg"));
|
|
17207
|
+
add("hips", gates.loco.hips, num("hips_ratio"));
|
|
17208
|
+
} else if (kind === "idle" || kind === "travel") {
|
|
17209
|
+
if (kind === "idle") add("still", gates.idle.still, num("speed"));
|
|
17210
|
+
add("blade", gates.idle.blade, num("blade_deg"));
|
|
17211
|
+
add("seam", gates.idle.seam, num("seam_deg"));
|
|
17212
|
+
add("hips", gates.idle.hips, num("hips_ratio"), "hips");
|
|
17213
|
+
} else if (kind === "oneshot") {
|
|
17214
|
+
const rise = num("rise_m") ?? 0, air = num("air_frames") ?? 0;
|
|
17215
|
+
if (air >= 3 && rise < 0.12) {
|
|
17216
|
+
subs.airlie = [0, 2];
|
|
17217
|
+
reasons.push(`contacts say airborne x${air} but rise ${rise.toFixed(2)} m`);
|
|
17218
|
+
}
|
|
17219
|
+
add("hips", gates.oneshot.hips, num("hips_ratio"));
|
|
17220
|
+
} else {
|
|
17221
|
+
add("seam", gates.freeform.seam, num("seam_deg"));
|
|
17222
|
+
}
|
|
17223
|
+
let total = 0, wsum = 0;
|
|
17224
|
+
for (const [s, w] of Object.values(subs)) {
|
|
17225
|
+
total += s * w;
|
|
17226
|
+
wsum += w;
|
|
17227
|
+
}
|
|
17228
|
+
const pts = wsum ? Math.round(100 * total / wsum) : 0;
|
|
17229
|
+
const zeros = Object.values(subs).filter(([s]) => s === 0).map(([, w]) => w);
|
|
17230
|
+
const cap = zeros.some((w) => w >= 3) || zeros.length >= 2 ? "FAIL" : zeros.length ? "WARN" : null;
|
|
17231
|
+
const subscores = {};
|
|
17232
|
+
for (const [k, [s]] of Object.entries(subs)) subscores[k] = Math.round(s * 100) / 100;
|
|
17233
|
+
return { pts, subscores, reasons, cap };
|
|
17234
|
+
}
|
|
17235
|
+
function analyzeTake(stem, buf, gates = DEFAULT_GATES) {
|
|
17236
|
+
const t = loadTake(buf);
|
|
17237
|
+
const { kind, expect } = classify(stem);
|
|
17238
|
+
const F = t.F;
|
|
17239
|
+
const lo = kind !== "oneshot" ? Math.min(RAMP, Math.max(4, Math.floor(F / 4))) : 8;
|
|
17240
|
+
const w = [lo, F - Math.min(TAIL, Math.max(2, Math.floor(F / 8)))];
|
|
17241
|
+
const C = charFrame(t.P, t.heading);
|
|
17242
|
+
const m = { frames: F, fps: t.fps, kind };
|
|
17243
|
+
m.twist_ok = stem.startsWith("skate-");
|
|
17244
|
+
m.slip_cms = mSlip(t, w);
|
|
17245
|
+
m.roughness = mRoughness(t, w);
|
|
17246
|
+
const g = mGround(t, w);
|
|
17247
|
+
m.pen_cm = g.pen;
|
|
17248
|
+
m.float_cm = g.float;
|
|
17249
|
+
m.hips_ratio = mHipsRatio(t, w);
|
|
17250
|
+
m.blade_deg = mBlade(C, w);
|
|
17251
|
+
m.headyaw_deg = mHeadyaw(t, w);
|
|
17252
|
+
m.aim = /aim|rifle|pistol/.test(stem);
|
|
17253
|
+
if (m.aim) {
|
|
17254
|
+
Object.assign(m, mAimhold(C, w));
|
|
17255
|
+
m.headpitch_deg = mHeadpitch(t, w);
|
|
17256
|
+
}
|
|
17257
|
+
const travel = mTravel(t, w);
|
|
17258
|
+
m.speed = travel.speed;
|
|
17259
|
+
if (kind === "loco") {
|
|
17260
|
+
m.dir_dot = travel.dir && expect ? travel.dir[0] * expect[0] + travel.dir[1] * expect[1] : 0;
|
|
17261
|
+
m.heading_std = mHeadingStd(t, w);
|
|
17262
|
+
m.speed_cv = mSpeedCv(t, w);
|
|
17263
|
+
}
|
|
17264
|
+
if (kind === "loco" || kind === "idle" || kind === "travel" || kind === "freeform") {
|
|
17265
|
+
m.seam_deg = mSeam(t, w, kind);
|
|
17266
|
+
}
|
|
17267
|
+
if (kind === "oneshot") {
|
|
17268
|
+
const o = mOneshot(t, w);
|
|
17269
|
+
m.rise_m = o.rise;
|
|
17270
|
+
m.air_frames = o.air;
|
|
17271
|
+
}
|
|
17272
|
+
const { pts, subscores, reasons, cap } = score(kind, m, gates);
|
|
17273
|
+
let verdict = pts >= gates.pass ? "PASS" : pts >= gates.warn ? "WARN" : "FAIL";
|
|
17274
|
+
if (cap === "WARN" && verdict === "PASS") verdict = "WARN";
|
|
17275
|
+
else if (cap === "FAIL") verdict = "FAIL";
|
|
17276
|
+
const metrics = {};
|
|
17277
|
+
for (const [k, v] of Object.entries(m)) {
|
|
17278
|
+
metrics[k] = typeof v === "number" ? Math.round(v * 1e3) / 1e3 : v;
|
|
17279
|
+
}
|
|
17280
|
+
return { take: stem, kind, score: pts, verdict, reasons, metrics, subscores, prompt: t.text.slice(0, 140) };
|
|
17281
|
+
}
|
|
17282
|
+
|
|
17283
|
+
// src/lib/motion/compile.ts
|
|
17284
|
+
var deg2 = Math.PI / 180;
|
|
17285
|
+
var D2 = Math.sqrt(0.5);
|
|
17286
|
+
var DEFAULT_MOTION_CONFIG = {
|
|
17287
|
+
rampFrames: 50,
|
|
17288
|
+
tailSkip: 10,
|
|
17289
|
+
idleLoop: { minFrames: 40, maxFrames: 70, step: 2 },
|
|
17290
|
+
headGazeTargetDeg: 6,
|
|
17291
|
+
aimPitchDeg: 40,
|
|
17292
|
+
aimCandidates: 3,
|
|
17293
|
+
hipsClampM: 5e-3,
|
|
17294
|
+
minDirDot: 0.9
|
|
17295
|
+
};
|
|
17296
|
+
var GAIT_BY_SUFFIX = [
|
|
17297
|
+
["forward", "forward", [0, 1]],
|
|
17298
|
+
["back", "back", [0, -1]],
|
|
17299
|
+
["strafe-left", "strafeLeft", [1, 0]],
|
|
17300
|
+
["strafe-right", "strafeRight", [-1, 0]],
|
|
17301
|
+
["fl", "forwardLeft", [D2, D2]],
|
|
17302
|
+
["fr", "forwardRight", [-D2, D2]],
|
|
17303
|
+
["bl", "backLeft", [D2, -D2]],
|
|
17304
|
+
["br", "backRight", [-D2, -D2]]
|
|
17305
|
+
];
|
|
17306
|
+
var round = (v, p) => {
|
|
17307
|
+
const m = 10 ** p;
|
|
17308
|
+
return Math.round(v * m) / m;
|
|
17309
|
+
};
|
|
17310
|
+
function pack(quatCyc, hipsPos, contacts, fps, speed, dxz, loop = true) {
|
|
17311
|
+
return {
|
|
17312
|
+
fps,
|
|
17313
|
+
frames: quatCyc.length,
|
|
17314
|
+
speed: round(speed, 3),
|
|
17315
|
+
dir: [round(dxz[0], 3), round(dxz[1], 3)],
|
|
17316
|
+
loop,
|
|
17317
|
+
localQuat: quatCyc.map((f) => f.map((q) => q.map((v) => round(v, 6)))),
|
|
17318
|
+
hipsPos: hipsPos.map((p) => p.map((v) => round(v, 5))),
|
|
17319
|
+
contacts
|
|
17320
|
+
};
|
|
17321
|
+
}
|
|
17322
|
+
var folded = (t) => t.fc.map((f) => [f[0] || f[1] ? 1 : 0, f[2] || f[3] ? 1 : 0]);
|
|
17323
|
+
function strikes(cc, col, lo, hi) {
|
|
17324
|
+
const out = [];
|
|
17325
|
+
for (let f = lo + 1; f < hi; f++) if (cc[f][col] === 1 && cc[f - 1][col] === 0) out.push(f);
|
|
17326
|
+
return out;
|
|
17327
|
+
}
|
|
17328
|
+
var slice2 = (a, s, e) => a.slice(s, e);
|
|
17329
|
+
var copyQuats = (q, s, e) => q.slice(s, e).map((f) => f.map((x) => x.slice()));
|
|
17330
|
+
var copyRoots = (r, s, e) => r.slice(s, e).map((v) => v.slice());
|
|
17331
|
+
function cutCycle(stem, t, expect, cfg) {
|
|
17332
|
+
const cc = folded(t);
|
|
17333
|
+
const lo = cfg.rampFrames, hi = t.F - cfg.tailSkip;
|
|
17334
|
+
const ls = strikes(cc, 0, lo, hi);
|
|
17335
|
+
if (ls.length < 3) {
|
|
17336
|
+
throw new Error(
|
|
17337
|
+
`${stem}: only ${ls.length} left-foot strikes in the steady window \u2014 the take never settles into a gait (longer --duration, or regenerate).`
|
|
17338
|
+
);
|
|
17339
|
+
}
|
|
17340
|
+
let best = null;
|
|
17341
|
+
for (let i = 0; i < ls.length - 1; i++) {
|
|
17342
|
+
const s2 = ls[i];
|
|
17343
|
+
for (const e2 of [ls[i + 1] - 1, ls[i + 1], ls[i + 1] + 1]) {
|
|
17344
|
+
if (e2 <= s2 + 6 || e2 >= hi) continue;
|
|
17345
|
+
const d = poseDist(t.quat, s2, e2);
|
|
17346
|
+
if (!best || d < best[0]) best = [d, s2, e2];
|
|
17347
|
+
}
|
|
17348
|
+
}
|
|
17349
|
+
if (!best) throw new Error(`${stem}: no valid strike pair for a loop cycle.`);
|
|
17350
|
+
const [seam, s, e] = best;
|
|
17351
|
+
const qc = copyQuats(t.quat, s, e);
|
|
17352
|
+
const rc = copyRoots(t.root, s, e);
|
|
17353
|
+
const { speed, dir } = deroot(qc, rc, t.fps, mean(t.heading.slice(s, e)));
|
|
17354
|
+
const dot = dir[0] * expect[0] + dir[1] * expect[1];
|
|
17355
|
+
if (dot <= cfg.minDirDot) {
|
|
17356
|
+
throw new Error(
|
|
17357
|
+
`${stem}: cycle travels (${dir[0].toFixed(2)}, ${dir[1].toFixed(2)}) vs expected (${expect[0].toFixed(2)}, ${expect[1].toFixed(2)}) \u2014 wrong direction; check the constraint/prompt.`
|
|
17358
|
+
);
|
|
17359
|
+
}
|
|
17360
|
+
return { track: pack(qc, rc, slice2(cc, s, e), t.fps, speed, dir), seamDeg: seam / deg2 };
|
|
17361
|
+
}
|
|
17362
|
+
function bestLoop(t, cfg) {
|
|
17363
|
+
const lo = 10, hi = t.F - 5;
|
|
17364
|
+
let best = null;
|
|
17365
|
+
for (let s = lo; s < hi - cfg.idleLoop.minFrames; s++) {
|
|
17366
|
+
for (let L = cfg.idleLoop.minFrames; L <= cfg.idleLoop.maxFrames; L += cfg.idleLoop.step) {
|
|
17367
|
+
const e = s + L;
|
|
17368
|
+
if (e >= hi) continue;
|
|
17369
|
+
const d = poseDist(t.quat, s, e);
|
|
17370
|
+
if (!best || d < best[0]) best = [d, s, e];
|
|
17371
|
+
}
|
|
17372
|
+
}
|
|
17373
|
+
if (!best) throw new Error("take too short for an idle loop window");
|
|
17374
|
+
return { seam: best[0], s: best[1], e: best[2] };
|
|
17375
|
+
}
|
|
17376
|
+
function compileIdle(stem, t, aim, cfg) {
|
|
17377
|
+
const log = [];
|
|
17378
|
+
const { seam, s, e } = bestLoop(t, cfg);
|
|
17379
|
+
const qc = copyQuats(t.quat, s, e);
|
|
17380
|
+
const rc = copyRoots(t.root, s, e);
|
|
17381
|
+
const cc = folded(t);
|
|
17382
|
+
if (!aim) {
|
|
17383
|
+
deroot(qc, rc, t.fps, mean(t.heading.slice(s, e)));
|
|
17384
|
+
return { track: pack(qc, rc, slice2(cc, s, e), t.fps, 0, [0, 0]), seamDeg: seam / deg2, log };
|
|
17385
|
+
}
|
|
17386
|
+
const { yaws, pitches } = gripLineChar(t.P, t.heading);
|
|
17387
|
+
const yawMed = median2(yaws.slice(s, e));
|
|
17388
|
+
const pitchMed = median2(pitches.slice(s, e));
|
|
17389
|
+
const headExcess = headPitchFrames(t.localMats, s, e) - cfg.headGazeTargetDeg;
|
|
17390
|
+
log.push(
|
|
17391
|
+
`idle root-yaw ${(-yawMed).toFixed(1)}\xB0 (whole body blades) \xB7 arm-level ${(-pitchMed).toFixed(1)}\xB0 \xB7 head-raise ${Math.max(0, headExcess).toFixed(1)}\xB0`
|
|
17392
|
+
);
|
|
17393
|
+
for (let f = 0; f < qc.length; f++) {
|
|
17394
|
+
qc[f] = levelArms(qc[f], t.P[s + f], pitchMed);
|
|
17395
|
+
if (headExcess > 0) qc[f] = levelHead(qc[f], t.P[s + f], headExcess);
|
|
17396
|
+
}
|
|
17397
|
+
deroot(qc, rc, t.fps, mean(t.heading.slice(s, e)) + yawMed * deg2);
|
|
17398
|
+
const over = mean(rc.map((r) => r[1])) - HIPS_REST_Y;
|
|
17399
|
+
if (over > cfg.hipsClampM) {
|
|
17400
|
+
log.push(`hips tall-stance clamp: -${over.toFixed(3)} m`);
|
|
17401
|
+
for (const r of rc) r[1] -= over;
|
|
17402
|
+
}
|
|
17403
|
+
return { track: pack(qc, rc, slice2(cc, s, e), t.fps, 0, [0, 0]), seamDeg: seam / deg2, log };
|
|
17404
|
+
}
|
|
17405
|
+
function cutOneshot(t, cfg) {
|
|
17406
|
+
const cc = folded(t);
|
|
17407
|
+
const air = cc.map((c2) => c2[0] === 0 && c2[1] === 0);
|
|
17408
|
+
let best = null;
|
|
17409
|
+
let run2 = null;
|
|
17410
|
+
for (let f = 0; f < air.length; f++) {
|
|
17411
|
+
if (air[f]) run2 = run2 ? [run2[0], f] : [f, f];
|
|
17412
|
+
else {
|
|
17413
|
+
if (run2 && (!best || run2[1] - run2[0] > best[1] - best[0])) best = run2;
|
|
17414
|
+
run2 = null;
|
|
17415
|
+
}
|
|
17416
|
+
}
|
|
17417
|
+
if (run2 && (!best || run2[1] - run2[0] > best[1] - best[0])) best = run2;
|
|
17418
|
+
let s, e;
|
|
17419
|
+
if (best) {
|
|
17420
|
+
s = best[0];
|
|
17421
|
+
while (s > 8 && !air[s - 1]) s--;
|
|
17422
|
+
e = Math.min(t.F - 4, Math.floor(best[1] + 0.6 * t.fps));
|
|
17423
|
+
} else {
|
|
17424
|
+
s = 8;
|
|
17425
|
+
e = t.F - 4;
|
|
17426
|
+
}
|
|
17427
|
+
void cfg;
|
|
17428
|
+
const qc = copyQuats(t.quat, s, e);
|
|
17429
|
+
const rc = copyRoots(t.root, s, e);
|
|
17430
|
+
deroot(qc, rc, t.fps, mean(t.heading.slice(s, Math.min(s + 10, e))));
|
|
17431
|
+
return pack(qc, rc, slice2(cc, s, e), t.fps, 0, [0, 0], false);
|
|
17432
|
+
}
|
|
17433
|
+
function aimPoseTracks(takes, cfg) {
|
|
17434
|
+
const I_HIPS4 = ji("Hips"), I_HEAD3 = ji("Head");
|
|
17435
|
+
const I_RH3 = ji("RightHand"), I_LH3 = ji("LeftHand");
|
|
17436
|
+
const cands = [];
|
|
17437
|
+
for (const { stem, take: t } of takes) {
|
|
17438
|
+
const { yaws, pitches } = gripLineChar(t.P, t.heading);
|
|
17439
|
+
const lo = Math.min(cfg.rampFrames, Math.floor(t.F / 3));
|
|
17440
|
+
const hiY = t.F - cfg.tailSkip;
|
|
17441
|
+
const yawMed = median2(yaws.slice(lo, hiY));
|
|
17442
|
+
const handVel = [];
|
|
17443
|
+
for (let f = 0; f < t.F - 1; f++) {
|
|
17444
|
+
const sp = (j) => Math.hypot(
|
|
17445
|
+
t.P[f + 1][j][0] - t.P[f][j][0],
|
|
17446
|
+
t.P[f + 1][j][1] - t.P[f][j][1],
|
|
17447
|
+
t.P[f + 1][j][2] - t.P[f][j][2]
|
|
17448
|
+
) * t.fps;
|
|
17449
|
+
handVel.push(Math.max(sp(I_RH3), sp(I_LH3)));
|
|
17450
|
+
}
|
|
17451
|
+
for (let f = lo; f < hiY; f++) {
|
|
17452
|
+
const rot = rotY(-t.heading[f]);
|
|
17453
|
+
const rel = (j) => matVec(rot, [
|
|
17454
|
+
t.P[f][j][0] - t.P[f][I_HIPS4][0],
|
|
17455
|
+
t.P[f][j][1] - t.P[f][I_HIPS4][1],
|
|
17456
|
+
t.P[f][j][2] - t.P[f][I_HIPS4][2]
|
|
17457
|
+
]);
|
|
17458
|
+
const rh = rel(I_RH3), lh = rel(I_LH3), head = rel(I_HEAD3), hips = rel(I_HIPS4);
|
|
17459
|
+
const up = [head[0] - hips[0], head[1] - hips[1], head[2] - hips[2]];
|
|
17460
|
+
const hunch = Math.acos(Math.max(-1, Math.min(1, up[1] / (Math.hypot(...up) + 1e-9)))) / deg2;
|
|
17461
|
+
const v = [lh[0] - rh[0], lh[1] - rh[1], lh[2] - rh[2]];
|
|
17462
|
+
const L = Math.hypot(...v);
|
|
17463
|
+
if (L < 0.15) continue;
|
|
17464
|
+
const yaw = Math.atan2(v[0], Math.max(v[2], 1e-6)) / deg2;
|
|
17465
|
+
const ry = yawMed * deg2;
|
|
17466
|
+
const fwdN = (v[0] * Math.sin(ry) + v[2] * Math.cos(ry)) / L;
|
|
17467
|
+
const dy = 0.5 * (rh[1] + lh[1]) - head[1];
|
|
17468
|
+
const score2 = fwdN - Math.abs(yaw - yawMed) / 30 - 4 * Math.max(0, dy + 0.25) - 2.5 * Math.abs(L - 0.32) - Math.max(0, hunch - 14) / 10 - 0.5 * Math.min(handVel[Math.min(f, handVel.length - 1)], 1);
|
|
17469
|
+
cands.push({ score: score2, stem, f, take: t, yaw, pitch: pitches[f] });
|
|
17470
|
+
}
|
|
17471
|
+
}
|
|
17472
|
+
cands.sort((a, b) => b.score - a.score);
|
|
17473
|
+
const picked = [];
|
|
17474
|
+
for (const c2 of cands) {
|
|
17475
|
+
if (picked.some((p) => p.stem === c2.stem)) continue;
|
|
17476
|
+
picked.push(c2);
|
|
17477
|
+
if (picked.length === cfg.aimCandidates) break;
|
|
17478
|
+
}
|
|
17479
|
+
const tracks = {};
|
|
17480
|
+
const log = [];
|
|
17481
|
+
const PITCH = cfg.aimPitchDeg * deg2;
|
|
17482
|
+
picked.forEach((c2, i) => {
|
|
17483
|
+
log.push(
|
|
17484
|
+
`aim${i}: ${c2.stem} frame ${c2.f} score ${c2.score >= 0 ? "+" : ""}${c2.score.toFixed(2)} spine-yaw ${(-c2.yaw).toFixed(1)}\xB0 arm-level ${(-c2.pitch).toFixed(1)}\xB0`
|
|
17485
|
+
);
|
|
17486
|
+
let q = levelArms(c2.take.quat[c2.f].map((x) => x.slice()), c2.take.P[c2.f], c2.pitch);
|
|
17487
|
+
const hx = headPitchFrames(c2.take.localMats, c2.f, c2.f + 1) - cfg.headGazeTargetDeg;
|
|
17488
|
+
if (hx > 0) q = levelHead(q, c2.take.P[c2.f], hx);
|
|
17489
|
+
q = yawed(q, -c2.yaw * deg2);
|
|
17490
|
+
const hip = [0, c2.take.P[c2.f][ji("Hips")][1], 0];
|
|
17491
|
+
for (const [suffix, qv] of [["", q], ["Up", pitched(q, -PITCH)], ["Dn", pitched(q, PITCH)]]) {
|
|
17492
|
+
tracks[`aim${i}${suffix}`] = pack([qv], [hip], [[0, 0]], 30, 0, [0, 0]);
|
|
17493
|
+
}
|
|
17494
|
+
});
|
|
17495
|
+
return { tracks, log };
|
|
17496
|
+
}
|
|
17497
|
+
function compileSet(inputs, setName, cfg = DEFAULT_MOTION_CONFIG) {
|
|
17498
|
+
const log = [];
|
|
17499
|
+
const gaits = {};
|
|
17500
|
+
const aimTakes = [];
|
|
17501
|
+
for (const { stem, take } of inputs) {
|
|
17502
|
+
const { kind } = classify(stem);
|
|
17503
|
+
const aim = /aim|rifle|pistol/.test(stem);
|
|
17504
|
+
if (aim && (kind === "idle" || kind === "freeform")) aimTakes.push({ stem, take });
|
|
17505
|
+
if (kind === "loco") {
|
|
17506
|
+
const hit = GAIT_BY_SUFFIX.find(
|
|
17507
|
+
([suffix]) => stem.replace(/-s\d+$/, "").endsWith(`-${suffix}`) || stem.includes(`-run-${suffix}`)
|
|
17508
|
+
);
|
|
17509
|
+
if (!hit) throw new Error(`${stem}: locomotion take with no recognizable direction suffix.`);
|
|
17510
|
+
const { track, seamDeg } = cutCycle(stem, take, hit[2], cfg);
|
|
17511
|
+
gaits[hit[1]] = track;
|
|
17512
|
+
log.push(`${hit[1]}: ${track.frames} frames speed ${track.speed} m/s seam ${seamDeg.toFixed(1)}\xB0`);
|
|
17513
|
+
} else if (kind === "idle") {
|
|
17514
|
+
const { track, seamDeg, log: l } = compileIdle(stem, take, aim, cfg);
|
|
17515
|
+
gaits.idle = track;
|
|
17516
|
+
log.push(`idle: ${track.frames} frames seam ${seamDeg.toFixed(1)}\xB0`, ...l);
|
|
17517
|
+
} else if (kind === "oneshot") {
|
|
17518
|
+
const clean2 = stem.replace(/-s\d+$/, "");
|
|
17519
|
+
const name = /jump.*run|run.*jump/.test(clean2) ? "jumpRun" : /jump/.test(clean2) ? "jumpStand" : clean2.replace(/[^a-zA-Z0-9]+(.)/g, (_, ch) => ch.toUpperCase());
|
|
17520
|
+
const track = cutOneshot(take, cfg);
|
|
17521
|
+
gaits[name] = track;
|
|
17522
|
+
log.push(`${name}: ${track.frames} frames (one-shot)`);
|
|
17523
|
+
} else if (kind === "travel" || kind === "freeform") {
|
|
17524
|
+
const { track, seamDeg } = compileIdle(stem, take, false, cfg);
|
|
17525
|
+
const name = stem.replace(/-s\d+$/, "").replace(/[^a-zA-Z0-9]+(.)/g, (_, ch) => ch.toUpperCase());
|
|
17526
|
+
gaits[name] = track;
|
|
17527
|
+
log.push(`${name}: ${track.frames} frames seam ${seamDeg.toFixed(1)}\xB0 (freeform loop)`);
|
|
17528
|
+
}
|
|
17529
|
+
}
|
|
17530
|
+
if (aimTakes.length > 0) {
|
|
17531
|
+
const { tracks, log: l } = aimPoseTracks(aimTakes, cfg);
|
|
17532
|
+
Object.assign(gaits, tracks);
|
|
17533
|
+
log.push(...l);
|
|
17534
|
+
}
|
|
17535
|
+
return {
|
|
17536
|
+
data: {
|
|
17537
|
+
version: 1,
|
|
17538
|
+
set: setName,
|
|
17539
|
+
skeleton: "core27",
|
|
17540
|
+
joints: [...JOINTS],
|
|
17541
|
+
parents: [...PARENTS],
|
|
17542
|
+
restOffsets: REST_OFFSETS.map((o) => [...o]),
|
|
17543
|
+
restPositions: REST_POSITIONS.map((o) => [...o]),
|
|
17544
|
+
hipsRestY: HIPS_REST_Y,
|
|
17545
|
+
gaits
|
|
17546
|
+
},
|
|
17547
|
+
log
|
|
17548
|
+
};
|
|
17549
|
+
}
|
|
17550
|
+
|
|
17551
|
+
// src/lib/motion/constraints.ts
|
|
17552
|
+
var FPS = 20;
|
|
17553
|
+
var RAMP_S = 1.5;
|
|
17554
|
+
var WAYPOINT_EVERY = 10;
|
|
17555
|
+
var FIRST_FRAME = 10;
|
|
17556
|
+
var D3 = Math.sqrt(0.5);
|
|
17557
|
+
var MOTION_DIRECTIONS = {
|
|
17558
|
+
forward: [0, 1],
|
|
17559
|
+
back: [0, -1],
|
|
17560
|
+
"strafe-left": [1, 0],
|
|
17561
|
+
"strafe-right": [-1, 0],
|
|
17562
|
+
"forward-left": [D3, D3],
|
|
17563
|
+
"forward-right": [-D3, D3],
|
|
17564
|
+
"back-left": [D3, -D3],
|
|
17565
|
+
"back-right": [-D3, -D3]
|
|
17566
|
+
};
|
|
17567
|
+
function distAt(t, speed) {
|
|
17568
|
+
if (t <= RAMP_S) return 0.5 * speed * t * t / RAMP_S;
|
|
17569
|
+
return 0.5 * speed * RAMP_S + speed * (t - RAMP_S);
|
|
17570
|
+
}
|
|
17571
|
+
function directionConstraint(dir, speed, durationS) {
|
|
17572
|
+
const frames = [];
|
|
17573
|
+
const pts = [];
|
|
17574
|
+
const nFrames = Math.floor(durationS * FPS);
|
|
17575
|
+
for (let f = FIRST_FRAME; f <= nFrames; f += WAYPOINT_EVERY) {
|
|
17576
|
+
const t = f / FPS;
|
|
17577
|
+
const d = distAt(t, speed);
|
|
17578
|
+
frames.push(f);
|
|
17579
|
+
pts.push([Math.round(dir[0] * d * 1e4) / 1e4, Math.round(dir[1] * d * 1e4) / 1e4]);
|
|
17580
|
+
}
|
|
17581
|
+
return [
|
|
17582
|
+
{
|
|
17583
|
+
type: "root2d",
|
|
17584
|
+
frame_indices: frames,
|
|
17585
|
+
root_2d: pts,
|
|
17586
|
+
global_root_heading: frames.map(() => 0)
|
|
17587
|
+
}
|
|
17588
|
+
];
|
|
17589
|
+
}
|
|
17590
|
+
|
|
17591
|
+
// src/commands/motion.ts
|
|
17592
|
+
var MOTION_USAGE = `${c.bold("Options for `motion` (custom character animations \u2014 text \u2192 CoreSkeleton27 takes \u2192 compiled set)")}
|
|
17593
|
+
motion gen "<prompt>" Generate N raw motion takes + the objective verify
|
|
17594
|
+
report on the GPU service (bills per take; default 4).
|
|
17595
|
+
[--takes 1-8] [--duration 2-10 s] [--seeds 0,1,2]
|
|
17596
|
+
[--constraints file.json] [--no-wait]
|
|
17597
|
+
motion verify <takes\u2026> LOCALLY score .npz takes/dirs against the objective
|
|
17598
|
+
gates (same math the service ran \u2014 reproducible).
|
|
17599
|
+
[--gates gates.json] [--json]
|
|
17600
|
+
motion compile <takes\u2026> --out src/motion/<set>.json \u2014 cut loop cycles,
|
|
17601
|
+
deroot, grip-line normalize, pack the game-ready set.
|
|
17602
|
+
Take FILENAMES drive routing (rifle-run-forward.npz,
|
|
17603
|
+
rifle-idle.npz, jump-stand.npz\u2026). [--set <name>]
|
|
17604
|
+
[--config motion.config.json]
|
|
17605
|
+
motion install Vendor the motion runtime INTO this game: retarget
|
|
17606
|
+
formulas (src/motion/rigs.js), the clip player
|
|
17607
|
+
(anim-runtime.js), and motion.config.json \u2014 files the
|
|
17608
|
+
agent then owns and can edit. [--set rifle: also drop
|
|
17609
|
+
the proven rifle starter set] [--force]
|
|
17610
|
+
motion constraints Write an ARDY root-2d constraint JSON (heading-pinned
|
|
17611
|
+
straight run) for gen. [--dir forward|back|strafe-left|
|
|
17612
|
+
strafe-right|forward-left|\u2026] [--speed 2.5] [--duration 5]
|
|
17613
|
+
[--out constraints.json]`;
|
|
17614
|
+
async function motionGen(opts, log) {
|
|
17615
|
+
const prompt = opts.query?.trim();
|
|
17616
|
+
if (!prompt) {
|
|
17617
|
+
log.error(`Missing prompt. Usage: ${c.cyan('genex motion gen "<prompt>"')}`);
|
|
17618
|
+
process.exitCode = 1;
|
|
17619
|
+
return;
|
|
17620
|
+
}
|
|
17621
|
+
const generationOptions = {};
|
|
17622
|
+
if (opts.takes !== void 0) generationOptions.takes = opts.takes;
|
|
17623
|
+
if (opts.duration !== void 0) generationOptions.duration = opts.duration;
|
|
17624
|
+
if (opts.seeds !== void 0) {
|
|
17625
|
+
const seeds = opts.seeds.split(",").map((s) => Number(s.trim()));
|
|
17626
|
+
if (seeds.length === 0 || seeds.length > 8 || seeds.some((s) => !Number.isInteger(s) || s < 0)) {
|
|
17627
|
+
log.error(`Invalid --seeds value: ${opts.seeds} (expected 1-8 comma-separated non-negative integers).`);
|
|
17628
|
+
process.exitCode = 1;
|
|
17629
|
+
return;
|
|
17630
|
+
}
|
|
17631
|
+
generationOptions.seeds = seeds;
|
|
17632
|
+
}
|
|
17633
|
+
if (opts.constraintsPath !== void 0) {
|
|
17634
|
+
try {
|
|
17635
|
+
const raw = await fs18.readFile(opts.constraintsPath, "utf8");
|
|
17636
|
+
generationOptions.constraints = JSON.parse(raw);
|
|
17637
|
+
} catch {
|
|
17638
|
+
log.error(`Couldn't read the --constraints JSON at ${opts.constraintsPath}.`);
|
|
17639
|
+
process.exitCode = 1;
|
|
17640
|
+
return;
|
|
17641
|
+
}
|
|
17642
|
+
}
|
|
17643
|
+
const genOpts = {
|
|
17644
|
+
prompt,
|
|
17645
|
+
generationOptions,
|
|
17646
|
+
apiUrl: opts.apiUrl,
|
|
17647
|
+
envPath: opts.envPath,
|
|
17648
|
+
token: opts.token,
|
|
17649
|
+
noWait: opts.noWait,
|
|
17650
|
+
json: opts.json,
|
|
17651
|
+
quiet: opts.quiet,
|
|
17652
|
+
open: opts.open
|
|
17653
|
+
};
|
|
17654
|
+
await runGenerate("motion", genOpts);
|
|
17655
|
+
}
|
|
17656
|
+
async function expandTakes(selectors) {
|
|
17657
|
+
const out = [];
|
|
17658
|
+
for (const sel of selectors) {
|
|
17659
|
+
const st = await fs18.stat(sel).catch(() => null);
|
|
17660
|
+
if (st?.isDirectory()) {
|
|
17661
|
+
const names = await fs18.readdir(sel);
|
|
17662
|
+
for (const n of names.sort()) if (n.endsWith(".npz")) out.push(path18.join(sel, n));
|
|
17663
|
+
} else if (st?.isFile()) {
|
|
17664
|
+
out.push(sel);
|
|
17665
|
+
} else {
|
|
17666
|
+
throw new Error(`No such take file or directory: ${sel}`);
|
|
17667
|
+
}
|
|
17668
|
+
}
|
|
17669
|
+
return out;
|
|
17670
|
+
}
|
|
17671
|
+
function mergeGates(base, patch) {
|
|
17672
|
+
const out = structuredClone(base);
|
|
17673
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
17674
|
+
if (k === "pass" || k === "warn") {
|
|
17675
|
+
if (typeof v === "number") out[k] = v;
|
|
17676
|
+
} else if (v && typeof v === "object" && k in out) {
|
|
17677
|
+
const section = out[k];
|
|
17678
|
+
for (const [name, band] of Object.entries(v)) {
|
|
17679
|
+
if (band && typeof band === "object") {
|
|
17680
|
+
section[name] = { ...section[name], ...band };
|
|
17681
|
+
}
|
|
17682
|
+
}
|
|
17683
|
+
}
|
|
17684
|
+
}
|
|
17685
|
+
return out;
|
|
17686
|
+
}
|
|
17687
|
+
async function motionVerify(opts, log) {
|
|
17688
|
+
const selectors = opts.selectors ?? [];
|
|
17689
|
+
if (selectors.length === 0) {
|
|
17690
|
+
log.error(`Missing takes. Usage: ${c.cyan("genex motion verify <take.npz\u2026|dir> [--gates gates.json] [--json]")}`);
|
|
17691
|
+
process.exitCode = 1;
|
|
17692
|
+
return;
|
|
17693
|
+
}
|
|
17694
|
+
let gates = DEFAULT_GATES;
|
|
17695
|
+
if (opts.gatesPath) {
|
|
17696
|
+
try {
|
|
17697
|
+
gates = mergeGates(DEFAULT_GATES, JSON.parse(await fs18.readFile(opts.gatesPath, "utf8")));
|
|
17698
|
+
} catch {
|
|
17699
|
+
log.error(`Couldn't read the --gates JSON at ${opts.gatesPath}.`);
|
|
17700
|
+
process.exitCode = 1;
|
|
17701
|
+
return;
|
|
17702
|
+
}
|
|
17703
|
+
}
|
|
17704
|
+
let files;
|
|
17705
|
+
try {
|
|
17706
|
+
files = await expandTakes(selectors);
|
|
17707
|
+
} catch (err) {
|
|
17708
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
17709
|
+
process.exitCode = 1;
|
|
17710
|
+
return;
|
|
17711
|
+
}
|
|
17712
|
+
if (files.length === 0) {
|
|
17713
|
+
log.error("No .npz takes found.");
|
|
17714
|
+
process.exitCode = 1;
|
|
17715
|
+
return;
|
|
17716
|
+
}
|
|
17717
|
+
const reports = [];
|
|
17718
|
+
for (const file of files) {
|
|
17719
|
+
const stem = path18.basename(file).replace(/\.npz$/, "");
|
|
17720
|
+
try {
|
|
17721
|
+
reports.push(analyzeTake(stem, await fs18.readFile(file), gates));
|
|
17722
|
+
} catch (err) {
|
|
17723
|
+
reports.push({
|
|
17724
|
+
take: stem,
|
|
17725
|
+
kind: "?",
|
|
17726
|
+
score: 0,
|
|
17727
|
+
verdict: "ERROR",
|
|
17728
|
+
reasons: [err instanceof Error ? err.message : String(err)],
|
|
17729
|
+
metrics: {},
|
|
17730
|
+
subscores: {},
|
|
17731
|
+
prompt: ""
|
|
17732
|
+
});
|
|
17733
|
+
}
|
|
17734
|
+
}
|
|
17735
|
+
reports.sort((a, b) => a.score - b.score);
|
|
17736
|
+
if (opts.json) {
|
|
17737
|
+
writeJson(reports);
|
|
17738
|
+
return;
|
|
17739
|
+
}
|
|
17740
|
+
const wide = Math.max(...reports.map((r) => r.take.length));
|
|
17741
|
+
for (const r of reports) {
|
|
17742
|
+
const why = r.reasons.length ? ` \u2014 ${r.reasons.join("; ")}` : "";
|
|
17743
|
+
log.plain(`${r.verdict.padEnd(5)} ${String(r.score).padStart(3)} ${r.take.padEnd(wide)} [${r.kind}]${why}`);
|
|
17744
|
+
}
|
|
17745
|
+
log.plain("");
|
|
17746
|
+
log.dim(" The report is data, not a verdict on the verb \u2014 pick the takes whose motion");
|
|
17747
|
+
log.dim(" reads right, then: genex motion compile <picked\u2026> --out src/motion/<set>.json");
|
|
17748
|
+
}
|
|
17749
|
+
async function motionCompile(opts, log) {
|
|
17750
|
+
const selectors = opts.selectors ?? [];
|
|
17751
|
+
if (selectors.length === 0 || !opts.out) {
|
|
17752
|
+
log.error(`Usage: ${c.cyan("genex motion compile <take.npz\u2026|dir> --out src/motion/<set>.json [--config motion.config.json]")}`);
|
|
17753
|
+
process.exitCode = 1;
|
|
17754
|
+
return;
|
|
17755
|
+
}
|
|
17756
|
+
let cfg = DEFAULT_MOTION_CONFIG;
|
|
17757
|
+
if (opts.configPath) {
|
|
17758
|
+
try {
|
|
17759
|
+
const patch = JSON.parse(await fs18.readFile(opts.configPath, "utf8"));
|
|
17760
|
+
cfg = { ...DEFAULT_MOTION_CONFIG, ...patch, idleLoop: { ...DEFAULT_MOTION_CONFIG.idleLoop, ...patch.idleLoop } };
|
|
17761
|
+
} catch {
|
|
17762
|
+
log.error(`Couldn't read the --config JSON at ${opts.configPath}.`);
|
|
17763
|
+
process.exitCode = 1;
|
|
17764
|
+
return;
|
|
17765
|
+
}
|
|
17766
|
+
}
|
|
17767
|
+
let files;
|
|
17768
|
+
try {
|
|
17769
|
+
files = await expandTakes(selectors);
|
|
17770
|
+
} catch (err) {
|
|
17771
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
17772
|
+
process.exitCode = 1;
|
|
17773
|
+
return;
|
|
17774
|
+
}
|
|
17775
|
+
const inputs = [];
|
|
17776
|
+
for (const file of files) {
|
|
17777
|
+
const stem = path18.basename(file).replace(/\.npz$/, "");
|
|
17778
|
+
try {
|
|
17779
|
+
inputs.push({ stem, take: loadTake(await fs18.readFile(file)) });
|
|
17780
|
+
} catch (err) {
|
|
17781
|
+
log.error(`${stem}: ${err instanceof Error ? err.message : String(err)}`);
|
|
17782
|
+
process.exitCode = 1;
|
|
17783
|
+
return;
|
|
17784
|
+
}
|
|
17785
|
+
}
|
|
17786
|
+
const setName = opts.set ?? path18.basename(opts.out).replace(/\.json$/, "");
|
|
17787
|
+
let result;
|
|
17788
|
+
try {
|
|
17789
|
+
result = compileSet(inputs, setName, cfg);
|
|
17790
|
+
} catch (err) {
|
|
17791
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
17792
|
+
process.exitCode = 1;
|
|
17793
|
+
return;
|
|
17794
|
+
}
|
|
17795
|
+
const trackCount = Object.keys(result.data.gaits).length;
|
|
17796
|
+
if (trackCount === 0) {
|
|
17797
|
+
log.error("No tracks compiled \u2014 check the take filenames (routing is by stem: rifle-run-forward, rifle-idle, jump-stand\u2026).");
|
|
17798
|
+
process.exitCode = 1;
|
|
17799
|
+
return;
|
|
17800
|
+
}
|
|
17801
|
+
await fs18.mkdir(path18.dirname(path18.resolve(opts.out)), { recursive: true });
|
|
17802
|
+
const json = JSON.stringify(result.data);
|
|
17803
|
+
await fs18.writeFile(opts.out, json);
|
|
17804
|
+
if (opts.json) {
|
|
17805
|
+
writeJson({ out: opts.out, set: setName, tracks: Object.keys(result.data.gaits), bytes: json.length });
|
|
17806
|
+
return;
|
|
17807
|
+
}
|
|
17808
|
+
for (const line of result.log) log.dim(` ${line}`);
|
|
17809
|
+
log.success(`Wrote ${opts.out} (${(json.length / 1e3).toFixed(0)} KB, ${trackCount} tracks).`);
|
|
17810
|
+
log.dim(" Play it with the vendored runtime (genex motion install) \u2014 see the genex-ai-character skill's motion reference.");
|
|
17811
|
+
}
|
|
17812
|
+
var MOTION_RUNTIME_FILES = [
|
|
17813
|
+
"rigs.js",
|
|
17814
|
+
"anim-runtime.js",
|
|
17815
|
+
"ik.js",
|
|
17816
|
+
"motion.config.json"
|
|
17817
|
+
];
|
|
17818
|
+
var MOTION_PRESETS = {
|
|
17819
|
+
rifle: ["sets/rifle.json", "sets/jumps.json"]
|
|
17820
|
+
};
|
|
17821
|
+
var MOTION_DEST = path18.join("src", "motion");
|
|
17822
|
+
async function motionInstall(opts, log) {
|
|
17823
|
+
const srcDir = path18.join(getTemplatesDir(), "motion");
|
|
17824
|
+
const root = opts.cwd ?? process.cwd();
|
|
17825
|
+
const preset = opts.set;
|
|
17826
|
+
if (preset !== void 0 && !MOTION_PRESETS[preset]) {
|
|
17827
|
+
log.error(`Unknown motion preset "${preset}". Available: ${Object.keys(MOTION_PRESETS).join(", ")}.`);
|
|
17828
|
+
process.exitCode = 1;
|
|
17829
|
+
return;
|
|
17830
|
+
}
|
|
17831
|
+
const files = [...MOTION_RUNTIME_FILES, ...preset ? MOTION_PRESETS[preset] : []];
|
|
17832
|
+
log.plain(c.bold(`genex motion install${preset ? ` --set ${preset}` : ""}`));
|
|
17833
|
+
log.plain("");
|
|
17834
|
+
log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST + path18.sep)}`);
|
|
17835
|
+
let copied = 0, skipped = 0;
|
|
17836
|
+
try {
|
|
17837
|
+
for (const rel of files) {
|
|
17838
|
+
const dest = path18.join(root, MOTION_DEST, rel);
|
|
17839
|
+
const exists4 = await fs18.access(dest).then(() => true, () => false);
|
|
17840
|
+
if (!opts.force && exists4) {
|
|
17841
|
+
skipped++;
|
|
17842
|
+
log.dim(` skipped ${path18.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
|
|
17843
|
+
continue;
|
|
17844
|
+
}
|
|
17845
|
+
await fs18.mkdir(path18.dirname(dest), { recursive: true });
|
|
17846
|
+
await fs18.copyFile(path18.join(srcDir, rel), dest);
|
|
17847
|
+
copied++;
|
|
17848
|
+
log.dim(` ${path18.join(MOTION_DEST, rel)}`);
|
|
17849
|
+
}
|
|
17850
|
+
} catch (err) {
|
|
17851
|
+
log.error(`Copy failed: ${String(err)}`);
|
|
17852
|
+
process.exitCode = 1;
|
|
17853
|
+
return;
|
|
17854
|
+
}
|
|
17855
|
+
log.success(`Motion runtime ready (${copied} copied${skipped > 0 ? `, ${skipped} skipped` : ""}).`);
|
|
17856
|
+
log.plain("");
|
|
17857
|
+
log.plain(c.bold("Next steps"));
|
|
17858
|
+
log.plain(" 1. No new dependencies for GLB rigs (three is in the scaffold); VRM avatars also need @pixiv/three-vrm.");
|
|
17859
|
+
log.plain(` 2. Read the ${c.cyan("genex-ai-character")} skill's motion reference for wiring, the prompt wording ladder, and the gen \u2192 verify \u2192 compile loop.`);
|
|
17860
|
+
log.plain(" 3. Wiring sketch:");
|
|
17861
|
+
for (const line of [
|
|
17862
|
+
`const rig = await loadRig("./assets/character.glb", set, scene); rig.computeCorrection();`,
|
|
17863
|
+
`const restAnkle = captureRestAnkle(rig, scene); reanchorFeet(rig, set, scene); // BEFORE ClipSet`,
|
|
17864
|
+
`const clipSet = new ClipSet(rig, set, set.gaits); const anim = new Animator(clipSet);`,
|
|
17865
|
+
`groundCalibrate(rig, clipSet, scene, restAnkle); curlFingers(rig, scene); // once, at load`,
|
|
17866
|
+
`anim.setLoops({ idle: 1 - mag, ...dir8Weights(x, z, mag) }, dt); maskW = aimMaskStep(maskW, { mag, grounded, pitch, dt });`,
|
|
17867
|
+
`anim.setMask("aim0", maskW, pitch / 0.6); anim.update(dt); // per frame \u2014 src/motion/ is YOURS to tune`
|
|
17868
|
+
]) {
|
|
17869
|
+
log.dim(` ${line}`);
|
|
17870
|
+
}
|
|
17871
|
+
}
|
|
17872
|
+
async function motionConstraints(opts, log) {
|
|
17873
|
+
const dirName = opts.dir ?? "forward";
|
|
17874
|
+
const dir = MOTION_DIRECTIONS[dirName];
|
|
17875
|
+
if (!dir) {
|
|
17876
|
+
log.error(`Unknown --dir "${dirName}". Directions: ${Object.keys(MOTION_DIRECTIONS).join(", ")}.`);
|
|
17877
|
+
process.exitCode = 1;
|
|
17878
|
+
return;
|
|
17879
|
+
}
|
|
17880
|
+
const speed = opts.speed ?? 2.5;
|
|
17881
|
+
const duration = opts.duration ?? 5;
|
|
17882
|
+
if (!(speed > 0) || !(duration >= 2 && duration <= 10)) {
|
|
17883
|
+
log.error("Expected --speed > 0 and --duration 2-10 seconds.");
|
|
17884
|
+
process.exitCode = 1;
|
|
17885
|
+
return;
|
|
17886
|
+
}
|
|
17887
|
+
const doc = directionConstraint(dir, speed, duration);
|
|
17888
|
+
const out = opts.out ?? "constraints.json";
|
|
17889
|
+
await fs18.writeFile(out, JSON.stringify(doc));
|
|
17890
|
+
if (opts.json) {
|
|
17891
|
+
writeJson({ out, dir: dirName, speed, duration, waypoints: doc[0].frame_indices.length });
|
|
17892
|
+
return;
|
|
17893
|
+
}
|
|
17894
|
+
log.success(`Wrote ${out} \u2014 ${dirName} at ${speed} m/s for ${duration}s (heading pinned +Z).`);
|
|
17895
|
+
log.dim(` Use it with: ${c.cyan(`genex motion gen "<gait prompt>" --constraints ${out}`)}`);
|
|
17896
|
+
}
|
|
17897
|
+
async function runMotion(opts) {
|
|
17898
|
+
const log = createLogger({ quiet: opts.quiet || opts.json });
|
|
17899
|
+
switch (opts.sub?.trim()) {
|
|
17900
|
+
case "gen":
|
|
17901
|
+
await motionGen(opts, log);
|
|
17902
|
+
return;
|
|
17903
|
+
case "verify":
|
|
17904
|
+
await motionVerify(opts, log);
|
|
17905
|
+
return;
|
|
17906
|
+
case "compile":
|
|
17907
|
+
await motionCompile(opts, log);
|
|
17908
|
+
return;
|
|
17909
|
+
case "install":
|
|
17910
|
+
await motionInstall(opts, log);
|
|
17911
|
+
return;
|
|
17912
|
+
case "constraints":
|
|
17913
|
+
await motionConstraints(opts, log);
|
|
17914
|
+
return;
|
|
17915
|
+
default: {
|
|
17916
|
+
const bare = createLogger({ quiet: opts.quiet });
|
|
17917
|
+
bare.error(
|
|
17918
|
+
opts.sub ? `Unknown motion command "${opts.sub}". Commands: gen, verify, compile, install, constraints.` : 'Missing motion command. Usage: genex motion <gen|verify|compile|install|constraints> \u2014 e.g. genex motion gen "soldier low-ready rifle idle".'
|
|
17919
|
+
);
|
|
17920
|
+
process.exitCode = 1;
|
|
17921
|
+
}
|
|
17922
|
+
}
|
|
17923
|
+
}
|
|
17924
|
+
|
|
16347
17925
|
// src/index.ts
|
|
16348
17926
|
var GEN_KINDS = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "music", "voice", "texture", "image", "video"]);
|
|
16349
17927
|
var HELP = `${c.bold("genex")} \u2014 set up your project's agent workspace, authorize, and publish 3D games.
|
|
@@ -16386,6 +17964,14 @@ ${c.bold("Usage")}
|
|
|
16386
17964
|
bind library clips via --animation. No approval
|
|
16387
17965
|
ceremony, no player controller pack.
|
|
16388
17966
|
genex animations search "<intent>" Search Meshy's animation library by gameplay intent.
|
|
17967
|
+
genex motion <sub> [args] Custom character animations from text (ARDY):
|
|
17968
|
+
gen "<prompt>" (GPU service, bills per take) |
|
|
17969
|
+
verify <takes\u2026> | compile <takes\u2026> --out \u2026 |
|
|
17970
|
+
install [--set rifle] | constraints \u2014 the
|
|
17971
|
+
local toolkit + vendored runtime the agent
|
|
17972
|
+
owns. Catalog actions (genex animations
|
|
17973
|
+
search) stay the first stop; motion covers
|
|
17974
|
+
the verbs the catalog lacks.
|
|
16389
17975
|
genex wait <id> Attach to a generation enqueued with --no-wait and
|
|
16390
17976
|
print its asset URL(s) when it finishes. Safe to
|
|
16391
17977
|
re-run \u2014 it never creates a new generation.
|
|
@@ -16537,6 +18123,8 @@ ${c.bold("Options for `controller character` / legacy `controller anims`")}
|
|
|
16537
18123
|
|
|
16538
18124
|
${UI_USAGE}
|
|
16539
18125
|
|
|
18126
|
+
${MOTION_USAGE}
|
|
18127
|
+
|
|
16540
18128
|
${c.bold("Global")}
|
|
16541
18129
|
--quiet Reduce output.
|
|
16542
18130
|
-h, --help Show this help.
|
|
@@ -16585,6 +18173,10 @@ ${c.bold("Examples")}
|
|
|
16585
18173
|
genex controller anims sword pistol
|
|
16586
18174
|
genex controller touch
|
|
16587
18175
|
genex controller networked-physics
|
|
18176
|
+
genex motion gen "soldier rifle low-ready idle, fully upright, head held high" --takes 4 --no-wait
|
|
18177
|
+
genex motion verify takes/ --json
|
|
18178
|
+
genex motion compile takes/rifle-idle.npz takes/rifle-run-forward.npz --out src/motion/rifle.json
|
|
18179
|
+
genex motion install --set rifle
|
|
16588
18180
|
`;
|
|
16589
18181
|
function parseArgs(argv) {
|
|
16590
18182
|
const parsed = {
|
|
@@ -16646,7 +18238,15 @@ function parseArgs(argv) {
|
|
|
16646
18238
|
"--candidate",
|
|
16647
18239
|
"--approve-remesh",
|
|
16648
18240
|
"--category",
|
|
16649
|
-
"--limit"
|
|
18241
|
+
"--limit",
|
|
18242
|
+
// `genex motion` value flags.
|
|
18243
|
+
"--takes",
|
|
18244
|
+
"--seeds",
|
|
18245
|
+
"--constraints",
|
|
18246
|
+
"--gates",
|
|
18247
|
+
"--config",
|
|
18248
|
+
"--set",
|
|
18249
|
+
"--speed"
|
|
16650
18250
|
]);
|
|
16651
18251
|
let i = 0;
|
|
16652
18252
|
while (i < argv.length) {
|
|
@@ -16773,6 +18373,12 @@ function parseArgs(argv) {
|
|
|
16773
18373
|
}
|
|
16774
18374
|
} else if (parsed.command === "creature") {
|
|
16775
18375
|
parsed.options.name = `${parsed.options.name} ${arg}`;
|
|
18376
|
+
} else if (parsed.command === "motion") {
|
|
18377
|
+
if (parsed.options.name === "gen") {
|
|
18378
|
+
parsed.options.query = parsed.options.query ? `${parsed.options.query} ${arg}` : arg;
|
|
18379
|
+
} else {
|
|
18380
|
+
(parsed.options.selectors ??= []).push(arg);
|
|
18381
|
+
}
|
|
16776
18382
|
} else if (parsed.command === "animations") {
|
|
16777
18383
|
parsed.options.query = parsed.options.query ? `${parsed.options.query} ${arg}` : arg;
|
|
16778
18384
|
} else {
|
|
@@ -16888,6 +18494,37 @@ function applyValueFlag(options, flag, value) {
|
|
|
16888
18494
|
case "--category":
|
|
16889
18495
|
options.category = value;
|
|
16890
18496
|
break;
|
|
18497
|
+
case "--takes": {
|
|
18498
|
+
const n = Number(value);
|
|
18499
|
+
if (!Number.isInteger(n) || n < 1 || n > 8) {
|
|
18500
|
+
throw new Error(`Invalid --takes value: ${value} (expected an integer 1-8)`);
|
|
18501
|
+
}
|
|
18502
|
+
options.takes = n;
|
|
18503
|
+
break;
|
|
18504
|
+
}
|
|
18505
|
+
case "--seeds":
|
|
18506
|
+
options.seeds = value;
|
|
18507
|
+
break;
|
|
18508
|
+
case "--constraints":
|
|
18509
|
+
options.constraintsPath = value;
|
|
18510
|
+
break;
|
|
18511
|
+
case "--gates":
|
|
18512
|
+
options.gatesPath = value;
|
|
18513
|
+
break;
|
|
18514
|
+
case "--config":
|
|
18515
|
+
options.configPath = value;
|
|
18516
|
+
break;
|
|
18517
|
+
case "--set":
|
|
18518
|
+
options.set = value;
|
|
18519
|
+
break;
|
|
18520
|
+
case "--speed": {
|
|
18521
|
+
const n = Number(value);
|
|
18522
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
18523
|
+
throw new Error(`Invalid --speed value: ${value}`);
|
|
18524
|
+
}
|
|
18525
|
+
options.speed = n;
|
|
18526
|
+
break;
|
|
18527
|
+
}
|
|
16891
18528
|
case "--limit": {
|
|
16892
18529
|
const n = Number(value);
|
|
16893
18530
|
if (!Number.isInteger(n) || n < 1 || n > 100) {
|
|
@@ -17114,6 +18751,9 @@ async function main() {
|
|
|
17114
18751
|
case "ui":
|
|
17115
18752
|
await runUi({ ...parsed.options, sub: parsed.options.name });
|
|
17116
18753
|
break;
|
|
18754
|
+
case "motion":
|
|
18755
|
+
await runMotion({ ...parsed.options, sub: parsed.options.name });
|
|
18756
|
+
break;
|
|
17117
18757
|
case "wait":
|
|
17118
18758
|
await runWait({ ...parsed.options, id: parsed.options.name });
|
|
17119
18759
|
break;
|