@genex-ai/cli-demo 0.36.0 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,7 +13,10 @@ genex model "<prompt>" # generate a 3D model → prints an asset URL
13
13
  genex skybox "<prompt>" # generate a 360° sky → prints an asset URL
14
14
  genex sfx "<prompt>" # generate a sound fx → prints an asset URL
15
15
  genex texture "<prompt>" # generate a texture → prints an asset URL
16
+ genex image "<prompt>" # generate an image → prints an asset URL
17
+ genex video "<prompt>" # generate a video → prints an asset URL
16
18
  genex controller <type> # install a tuned character|car|drone controller → src/controllers/
19
+ genex controller anims <sel…> # download extra character animation clips (by tag or name) → public/assets/anims/
17
20
  ```
18
21
 
19
22
  > **Invoking it.** First-time setup runs via `npx @genex-ai/cli-demo@latest init`.
@@ -72,8 +75,8 @@ Defaults: API `https://demo-api.glotech.world`, auth site
72
75
 
73
76
  ## Generating assets
74
77
 
75
- `genex model | skybox | sfx | texture "<prompt>"` turn a prompt into a real,
76
- game-ready asset stored in R2. The command blocks until the asset is ready (live SSE
78
+ `genex model | skybox | sfx | texture | image | video "<prompt>"` turn a prompt into a
79
+ real, game-ready asset stored in R2. The command blocks until the asset is ready (live SSE
77
80
  stream), then prints its public URL — the game loads it directly, nothing is
78
81
  downloaded or committed:
79
82
 
@@ -83,6 +86,8 @@ downloaded or committed:
83
86
  | `genex skybox "<prompt>"` | Blockade Labs | `…/generations/<id>/skybox-equirect` |
84
87
  | `genex sfx "<prompt>" [--duration <s>]` | ElevenLabs | `…/generations/<id>/audio-sfx` |
85
88
  | `genex texture "<prompt>" [--terrain]` | Gemini | `…/generations/<id>/texture-basecolor` |
89
+ | `genex image "<prompt>" [--transparent] [--aspect <ratio>]` | fal.ai | `…/generations/<id>/image-main` |
90
+ | `genex video "<prompt>" [--duration <s>] [--loop]` | fal.ai | `…/generations/<id>/video-mp4` |
86
91
 
87
92
  Each URL is a permanent `https://assets.genex.technology/...` address served straight
88
93
  from R2 (with CORS, so three.js loads it cross-origin without tainting). It resolves
@@ -92,9 +97,10 @@ Three.js loader code.
92
97
 
93
98
  Auth reuses the existing `GENEX_TOKEN` (run `genex init` first). Server-side, each
94
99
  provider is keyed by an env var (`TRIPO_API_KEY`, `BLOCKADE_LABS_API_KEY`,
95
- `ELEVENLABS_API_KEY`, `GEMINI_API_KEY`); when a key is unset that kind falls back to
96
- a built-in **mock** provider that returns sample assets, so the flow runs keyless.
97
- `--no-wait` enqueues without downloading.
100
+ `ELEVENLABS_API_KEY`, `GEMINI_API_KEY`, and `FAL_KEY` for image + video); when a key
101
+ is unset that kind falls back to a built-in **mock** provider that returns sample
102
+ assets, so the flow runs keyless. `--no-wait` enqueues without downloading — useful
103
+ for video, which is minutes-class end-to-end.
98
104
 
99
105
  ## Install / run
100
106
 
package/dist/index.js CHANGED
@@ -11,6 +11,15 @@ import { fileURLToPath } from "url";
11
11
  var DEFAULT_AUTH_URL = "https://demo-web.glotech.world";
12
12
  var DEFAULT_API_URL = "https://demo-api.glotech.world";
13
13
  var DEFAULT_COLYSEUS_URL = "wss://demo-colyseus.glotech.world";
14
+ var DEFAULT_ANIMS_BASE = "https://cdn.genex.technology/anims/ual1/v1/";
15
+ var ANIMS_BASE_ENV = "GENEX_ANIMS_BASE";
16
+ function getAnimsBase(override) {
17
+ const raw = override || process.env[ANIMS_BASE_ENV] || DEFAULT_ANIMS_BASE;
18
+ return raw.replace(/\/+$/, "") + "/";
19
+ }
20
+ function getAnimsCacheDir() {
21
+ return path.join(getGenexDir(), "cache", "anims");
22
+ }
14
23
  var ENV_TOKEN_KEY = "GENEX_TOKEN";
15
24
  var AUTH_URL_ENV = "GENEX_AUTH_URL";
16
25
  var API_URL_ENV = "GENEX_API_URL";
@@ -1790,6 +1799,10 @@ async function runGenerate(kind, opts) {
1790
1799
  const options = {};
1791
1800
  if (kind === "texture" && opts.terrain) options.terrain = true;
1792
1801
  if (kind === "sfx" && opts.duration) options.durationSeconds = opts.duration;
1802
+ if (kind === "image" && opts.transparent) options.transparent = true;
1803
+ if (kind === "image" && opts.aspect) options.aspect = opts.aspect;
1804
+ if (kind === "video" && opts.duration) options.durationSeconds = opts.duration;
1805
+ if (kind === "video" && opts.loop) options.loop = true;
1793
1806
  log.plain(c.bold(`genex ${kind}`));
1794
1807
  log.dim(` ${prompt}`);
1795
1808
  log.plain("");
@@ -1826,9 +1839,10 @@ async function runGenerate(kind, opts) {
1826
1839
  }
1827
1840
  log.step("Generating\u2026 (this can take up to a minute)");
1828
1841
  const onProgress = (p) => log.dim(` ${p}%`);
1829
- const deadline = Date.now() + WAIT_TIMEOUT_MS;
1842
+ const timeoutMs = waitTimeoutFor(kind);
1843
+ const deadline = Date.now() + timeoutMs;
1830
1844
  const streamed = await waitViaSSE(apiUrl, token, id, onProgress, deadline);
1831
- const view = streamed === "unsupported" ? await poll(apiUrl, token, id, onProgress) : streamed;
1845
+ const view = streamed === "unsupported" ? await poll(apiUrl, token, id, onProgress, timeoutMs) : streamed;
1832
1846
  if (!view) {
1833
1847
  log.error("Timed out waiting for the generation.");
1834
1848
  process.exitCode = 1;
@@ -1855,6 +1869,11 @@ async function runGenerate(kind, opts) {
1855
1869
  printHint(kind, files, log);
1856
1870
  }
1857
1871
  var WAIT_TIMEOUT_MS = 10 * 60 * 1e3;
1872
+ var KIND_WAIT_TIMEOUT_MS = {
1873
+ video: 15 * 60 * 1e3
1874
+ // 15 min
1875
+ };
1876
+ var waitTimeoutFor = (kind) => KIND_WAIT_TIMEOUT_MS[kind] ?? WAIT_TIMEOUT_MS;
1858
1877
  var TERMINAL = /* @__PURE__ */ new Set(["completed", "failed"]);
1859
1878
  async function waitViaSSE(apiUrl, token, id, onProgress, deadline) {
1860
1879
  let connectFailures = 0;
@@ -1907,8 +1926,8 @@ async function waitViaSSE(apiUrl, token, id, onProgress, deadline) {
1907
1926
  }
1908
1927
  return null;
1909
1928
  }
1910
- async function poll(apiUrl, token, id, onProgress) {
1911
- const deadline = Date.now() + WAIT_TIMEOUT_MS;
1929
+ async function poll(apiUrl, token, id, onProgress, timeoutMs) {
1930
+ const deadline = Date.now() + timeoutMs;
1912
1931
  let last = -1;
1913
1932
  while (Date.now() < deadline) {
1914
1933
  try {
@@ -1937,15 +1956,314 @@ function printHint(kind, files, log) {
1937
1956
  model: `Standard GLB \u2014 load with GLTFLoader straight from the URL \u2014 see the genex-ai-model skill. url = "${url}"`,
1938
1957
  skybox: `Load as an equirectangular texture \u2192 scene.background + scene.environment \u2014 see genex-ai-skybox. url = "${url}"`,
1939
1958
  sfx: `Load with AudioLoader into a THREE.PositionalAudio (camera needs an AudioListener) \u2014 see genex-ai-sfx. url = "${url}"`,
1940
- texture: `Load each map with TextureLoader (RepeatWrapping) into a MeshStandardMaterial \u2014 see genex-ai-texture. Use the URLs above by role.`
1959
+ texture: `Load each map with TextureLoader (RepeatWrapping) into a MeshStandardMaterial \u2014 see genex-ai-texture. Use the URLs above by role.`,
1960
+ image: `Load with TextureLoader (set colorSpace = SRGBColorSpace) onto any mesh/plane/sprite \u2014 see genex-ai-image. url = "${url}"`,
1961
+ video: `Wire an HTMLVideoElement (crossOrigin="anonymous", muted, loop, playsInline) into a THREE.VideoTexture \u2014 see genex-ai-video. url = "${url}"`
1941
1962
  };
1942
1963
  log.dim(` ${hint[kind]}`);
1943
1964
  log.dim(" Reference the URL directly in your code \u2014 don't download it into the repo.");
1944
1965
  }
1945
1966
 
1946
1967
  // src/commands/controller.ts
1968
+ import fs12 from "fs/promises";
1969
+ import path13 from "path";
1970
+
1971
+ // src/lib/anims.ts
1947
1972
  import fs11 from "fs/promises";
1948
1973
  import path12 from "path";
1974
+ var ANIMS_DEST = path12.join("public", "assets", "anims");
1975
+ var HIDDEN_TAG = "reference";
1976
+ async function runAnims(opts) {
1977
+ const log = createLogger({ quiet: opts.quiet });
1978
+ const root = opts.cwd ?? process.cwd();
1979
+ const selectors = opts.selectors ?? [];
1980
+ log.plain(c.bold("genex controller anims"));
1981
+ log.plain("");
1982
+ const { manifest, source } = await loadManifest(opts.animsBase);
1983
+ if (source === "snapshot") {
1984
+ log.dim(" (offline or CDN unreachable \u2014 using the bundled catalog snapshot)");
1985
+ }
1986
+ if (opts.list) {
1987
+ printCatalog(log, manifest, selectors);
1988
+ return;
1989
+ }
1990
+ const controllerMarker = path12.join(root, "src", "controllers", "character");
1991
+ if (!await exists2(controllerMarker)) {
1992
+ log.error(
1993
+ `No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
1994
+ );
1995
+ log.plain(` Run ${c.cyan("genex controller character")} first, then re-run this command.`);
1996
+ process.exitCode = 1;
1997
+ return;
1998
+ }
1999
+ const destDir = path12.join(root, ANIMS_DEST);
2000
+ const gameManifestPath = path12.join(destDir, "manifest.json");
2001
+ if (opts.reset) {
2002
+ await fs11.rm(destDir, { recursive: true, force: true });
2003
+ log.step(`Cleared ${c.cyan(ANIMS_DEST + path12.sep)} (--reset)`);
2004
+ }
2005
+ if (selectors.length === 0) {
2006
+ const installed = await readGameManifest(gameManifestPath);
2007
+ if (installed === null || installed.clips.length === 0) {
2008
+ log.plain(" No animation packs installed yet.");
2009
+ } else {
2010
+ log.plain(` Installed (${installed.clips.length} clips): ${installed.clips.join(", ")}`);
2011
+ }
2012
+ log.plain("");
2013
+ log.plain(
2014
+ ` Install with ${c.cyan("genex controller anims <tag|clip \u2026>")}; browse with ${c.cyan(
2015
+ "genex controller anims --list"
2016
+ )}.`
2017
+ );
2018
+ return;
2019
+ }
2020
+ let resolved;
2021
+ try {
2022
+ resolved = resolveSelectors(manifest, selectors);
2023
+ } catch (err) {
2024
+ log.error(err instanceof Error ? err.message : String(err));
2025
+ process.exitCode = 1;
2026
+ return;
2027
+ }
2028
+ const coreNames = new Set(manifest.core);
2029
+ const byName = /* @__PURE__ */ new Map();
2030
+ let bundledSkips = 0;
2031
+ for (const entries of resolved.values()) {
2032
+ for (const entry of entries) {
2033
+ if (coreNames.has(entry.name)) {
2034
+ bundledSkips++;
2035
+ continue;
2036
+ }
2037
+ byName.set(entry.name, entry);
2038
+ }
2039
+ }
2040
+ const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
2041
+ const cacheDir = path12.join(
2042
+ opts.cacheDir ?? getAnimsCacheDir(),
2043
+ `${manifest.library}-v${manifest.version}`
2044
+ );
2045
+ await fs11.mkdir(cacheDir, { recursive: true });
2046
+ await fs11.mkdir(destDir, { recursive: true });
2047
+ const base = getAnimsBase(opts.animsBase);
2048
+ let installedCount = 0;
2049
+ let presentCount = 0;
2050
+ let addedBytes = 0;
2051
+ const failures = [];
2052
+ for (const entry of wanted) {
2053
+ const dest = path12.join(destDir, entry.file);
2054
+ if (await hasSize(dest, entry.bytes)) {
2055
+ presentCount++;
2056
+ continue;
2057
+ }
2058
+ try {
2059
+ const cached = path12.join(cacheDir, entry.file);
2060
+ if (!await hasSize(cached, entry.bytes)) {
2061
+ const res = await fetch(base + entry.file);
2062
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
2063
+ const buf = Buffer.from(await res.arrayBuffer());
2064
+ await fs11.writeFile(cached, buf);
2065
+ }
2066
+ await fs11.copyFile(cached, dest);
2067
+ installedCount++;
2068
+ addedBytes += entry.bytes;
2069
+ log.dim(` ${path12.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
2070
+ } catch (err) {
2071
+ failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
2072
+ }
2073
+ }
2074
+ const previous = await readGameManifest(gameManifestPath);
2075
+ const union = new Set(previous?.clips ?? []);
2076
+ for (const entry of wanted) {
2077
+ if (!failures.some((f) => f.startsWith(`${entry.name} (`))) union.add(entry.name);
2078
+ }
2079
+ const gameManifest = {
2080
+ schema: 1,
2081
+ library: manifest.library,
2082
+ version: manifest.version,
2083
+ clips: [...union].sort((a, b) => a.localeCompare(b))
2084
+ };
2085
+ await fs11.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
2086
+ log.plain("");
2087
+ const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
2088
+ if (presentCount > 0) parts.push(`${presentCount} already present`);
2089
+ if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
2090
+ log.success(
2091
+ `${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path12.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
2092
+ );
2093
+ for (const [selector, entries] of resolved) {
2094
+ const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
2095
+ if (names.length > 0) log.dim(` ${selector}: ${names.join(", ")}`);
2096
+ }
2097
+ log.info(
2098
+ `Wiring: load the ${c.cyan("genex-threejs-character-controller")} skill \u2192 "Animation packs" (loadCharacterClips picks these up automatically).`
2099
+ );
2100
+ if (failures.length > 0) {
2101
+ log.plain("");
2102
+ log.error(
2103
+ `${failures.length} clip${failures.length === 1 ? "" : "s"} failed to download: ${failures.join(", ")}`
2104
+ );
2105
+ log.plain(
2106
+ " Each clip needs the network once per machine \u2014 check your connection and re-run the same command (already-installed clips are skipped)."
2107
+ );
2108
+ process.exitCode = 1;
2109
+ }
2110
+ }
2111
+ async function loadManifest(baseOverride) {
2112
+ const base = getAnimsBase(baseOverride);
2113
+ try {
2114
+ const res = await fetch(base + "manifest.json", { signal: AbortSignal.timeout(5e3) });
2115
+ if (res.ok) {
2116
+ const manifest2 = await res.json();
2117
+ if (manifest2.schema === 1 && Array.isArray(manifest2.clips)) {
2118
+ return { manifest: manifest2, source: "cdn" };
2119
+ }
2120
+ }
2121
+ } catch {
2122
+ }
2123
+ const snapshotPath = path12.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
2124
+ const manifest = JSON.parse(await fs11.readFile(snapshotPath, "utf8"));
2125
+ return { manifest, source: "snapshot" };
2126
+ }
2127
+ function resolveSelectors(manifest, selectors) {
2128
+ const byName = new Map(manifest.clips.map((entry) => [entry.name, entry]));
2129
+ const byLowerName = new Map(manifest.clips.map((entry) => [entry.name.toLowerCase(), entry]));
2130
+ const tags = /* @__PURE__ */ new Map();
2131
+ for (const entry of manifest.clips) {
2132
+ for (const tag of entry.tags) {
2133
+ const list = tags.get(tag) ?? [];
2134
+ list.push(entry);
2135
+ tags.set(tag, list);
2136
+ }
2137
+ }
2138
+ const out = /* @__PURE__ */ new Map();
2139
+ for (const selector of selectors) {
2140
+ const exact = byName.get(selector) ?? byLowerName.get(selector.toLowerCase());
2141
+ if (exact) {
2142
+ out.set(selector, [exact]);
2143
+ continue;
2144
+ }
2145
+ const tagHit = tags.get(selector) ?? tags.get(selector.toLowerCase());
2146
+ if (tagHit) {
2147
+ out.set(selector, tagHit);
2148
+ continue;
2149
+ }
2150
+ const candidates = [...tags.keys(), ...byName.keys()];
2151
+ const close = suggest(selector, candidates);
2152
+ throw new Error(
2153
+ `unknown clip/tag "${selector}"${close.length > 0 ? ` \u2014 closest: ${close.join(", ")}` : ""}. Run ${c.cyan(
2154
+ "genex controller anims --list"
2155
+ )} for the catalog.`
2156
+ );
2157
+ }
2158
+ return out;
2159
+ }
2160
+ function suggest(input, candidates) {
2161
+ const lower = input.toLowerCase();
2162
+ const scored = [];
2163
+ for (const candidate of candidates) {
2164
+ const candidateLower = candidate.toLowerCase();
2165
+ if (candidateLower.includes(lower) || lower.includes(candidateLower)) {
2166
+ scored.push({ name: candidate, score: 0 });
2167
+ continue;
2168
+ }
2169
+ const distance = levenshtein(lower, candidateLower, 2);
2170
+ if (distance <= 2) scored.push({ name: candidate, score: distance });
2171
+ }
2172
+ scored.sort((a, b) => a.score - b.score || a.name.localeCompare(b.name));
2173
+ return scored.slice(0, 4).map((s) => s.name);
2174
+ }
2175
+ function levenshtein(a, b, max) {
2176
+ if (Math.abs(a.length - b.length) > max) return max + 1;
2177
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
2178
+ for (let i = 1; i <= a.length; i++) {
2179
+ const curr = [i];
2180
+ let rowMin = i;
2181
+ for (let j = 1; j <= b.length; j++) {
2182
+ curr[j] = Math.min(
2183
+ prev[j] + 1,
2184
+ curr[j - 1] + 1,
2185
+ prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
2186
+ );
2187
+ if (curr[j] < rowMin) rowMin = curr[j];
2188
+ }
2189
+ if (rowMin > max) return max + 1;
2190
+ prev = curr;
2191
+ }
2192
+ return prev[b.length];
2193
+ }
2194
+ function printCatalog(log, manifest, selectors) {
2195
+ const coreNames = new Set(manifest.core);
2196
+ if (selectors.length > 0) {
2197
+ let resolved;
2198
+ try {
2199
+ resolved = resolveSelectors(manifest, selectors);
2200
+ } catch (err) {
2201
+ log.error(err instanceof Error ? err.message : String(err));
2202
+ process.exitCode = 1;
2203
+ return;
2204
+ }
2205
+ for (const [selector, entries] of resolved) {
2206
+ log.plain(c.bold(selector));
2207
+ for (const entry of entries) {
2208
+ const bundled = coreNames.has(entry.name) ? " (bundled)" : "";
2209
+ log.plain(
2210
+ ` ${entry.name.padEnd(26)} ${entry.duration.toFixed(1)}s ${formatMb(entry.bytes)}${bundled} ${c.dim(entry.desc)}`
2211
+ );
2212
+ }
2213
+ }
2214
+ return;
2215
+ }
2216
+ const tags = /* @__PURE__ */ new Map();
2217
+ for (const entry of manifest.clips) {
2218
+ for (const tag of entry.tags) {
2219
+ if (tag === HIDDEN_TAG) continue;
2220
+ const list = tags.get(tag) ?? [];
2221
+ list.push(entry);
2222
+ tags.set(tag, list);
2223
+ }
2224
+ }
2225
+ log.plain(
2226
+ `${c.bold(`Animation packs`)} (${manifest.library} v${manifest.version}, ${manifest.clips.length} clips)`
2227
+ );
2228
+ log.plain(
2229
+ ` Install: ${c.cyan("genex controller anims <tag|clip \u2026>")} Details: ${c.cyan(
2230
+ "genex controller anims --list <tag>"
2231
+ )}`
2232
+ );
2233
+ log.plain("");
2234
+ for (const [tag, entries] of [...tags.entries()].sort(([a], [b]) => a.localeCompare(b))) {
2235
+ const bytes = entries.reduce((sum, entry) => sum + entry.bytes, 0);
2236
+ const suffix = tag === "core" ? " \u2014 bundled in animation-library.glb" : ` (${formatMb(bytes)})`;
2237
+ log.plain(` ${c.bold(tag.padEnd(18))} ${entries.map((e) => e.name).join(", ")}${suffix}`);
2238
+ }
2239
+ }
2240
+ async function readGameManifest(file) {
2241
+ try {
2242
+ return JSON.parse(await fs11.readFile(file, "utf8"));
2243
+ } catch {
2244
+ return null;
2245
+ }
2246
+ }
2247
+ async function hasSize(file, bytes) {
2248
+ try {
2249
+ return (await fs11.stat(file)).size === bytes;
2250
+ } catch {
2251
+ return false;
2252
+ }
2253
+ }
2254
+ async function exists2(p) {
2255
+ try {
2256
+ await fs11.access(p);
2257
+ return true;
2258
+ } catch {
2259
+ return false;
2260
+ }
2261
+ }
2262
+ function formatMb(bytes) {
2263
+ return bytes >= 1e6 ? `${(bytes / 1e6).toFixed(1)} MB` : `${Math.round(bytes / 1e3)} KB`;
2264
+ }
2265
+
2266
+ // src/commands/controller.ts
1949
2267
  var CONTROLLER_KINDS = ["character", "car", "drone"];
1950
2268
  var SHARED = [
1951
2269
  "shared/math.ts",
@@ -1964,6 +2282,7 @@ var CONTROLLER_FILE_SETS = {
1964
2282
  ...SHARED,
1965
2283
  "character/character-controller.ts",
1966
2284
  "character/character-animations.ts",
2285
+ "character/animation-packs.ts",
1967
2286
  "character/presets.ts",
1968
2287
  // VRM avatar support (three-vrm): load + retarget the UAL clips + auto-fit
1969
2288
  // the capsule + optional foot IK. Owner's avatar replaces the old mannequin.
@@ -1976,15 +2295,16 @@ var CONTROLLER_FILE_SETS = {
1976
2295
  ],
1977
2296
  // The player's VRM is written to public/assets/avatar.vrm at install time by
1978
2297
  // installOwnerAvatar (owner's avatar, or the bundled default) — so it is NOT
1979
- // a static manifest asset. animation-library.glb (46 clips) still is.
2298
+ // a static manifest asset. animation-library.glb (the 12-clip core) still is;
2299
+ // extra clips arrive via `genex controller anims` into public/assets/anims/.
1980
2300
  assets: ["assets/animation-library.glb"],
1981
2301
  skill: "genex-threejs-character-controller",
1982
2302
  sketch: [
1983
2303
  `const physics = await PhysicsWorld.create();`,
1984
2304
  `const { scene, vrm } = await loadVrm("./assets/avatar.vrm");`,
1985
- `const lib = await new GLTFLoader().loadAsync("./assets/animation-library.glb");`,
2305
+ `const clips = await loadCharacterClips(vrm); // core library + every genex-controller-anims pack`,
1986
2306
  `const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...capsuleFromModel(scene), position: { x: 0, y: 2, z: 0 } });`,
1987
- `character.root.add(scene); const anims = new CharacterAnimations(scene, retargetClips(vrm, lib.scene, lib.animations));`,
2307
+ `character.root.add(scene); const anims = new CharacterAnimations(scene, clips);`,
1988
2308
  `addEventListener("pointerdown", () => anims.playOneShot("Punch_Jab")); // per frame: anims.update(character, dt); vrm.update(dt);`
1989
2309
  ]
1990
2310
  },
@@ -2024,45 +2344,49 @@ var CONTROLLER_FILE_SETS = {
2024
2344
  ]
2025
2345
  }
2026
2346
  };
2027
- var CODE_DEST = path12.join("src", "controllers");
2028
- var ASSETS_DEST = path12.join("public", "assets");
2347
+ var CODE_DEST = path13.join("src", "controllers");
2348
+ var ASSETS_DEST = path13.join("public", "assets");
2029
2349
  async function runController(opts) {
2030
2350
  const log = createLogger({ quiet: opts.quiet });
2351
+ if (opts.kind?.trim() === "anims") {
2352
+ await runAnims(opts);
2353
+ return;
2354
+ }
2031
2355
  const kind = opts.kind?.trim();
2032
2356
  if (!kind || !CONTROLLER_KINDS.includes(kind)) {
2033
2357
  log.error(
2034
2358
  `Missing or unknown controller type${kind ? ` "${kind}"` : ""}. Usage: ${c.cyan(
2035
2359
  "genex controller <character|car|drone> [--force]"
2036
- )}`
2360
+ )} or ${c.cyan("genex controller anims <tag|clip \u2026>")}`
2037
2361
  );
2038
2362
  process.exitCode = 1;
2039
2363
  return;
2040
2364
  }
2041
- const srcDir = path12.join(getTemplatesDir(), "controllers");
2365
+ const srcDir = path13.join(getTemplatesDir(), "controllers");
2042
2366
  const root = opts.cwd ?? process.cwd();
2043
2367
  const set = CONTROLLER_FILE_SETS[kind];
2044
2368
  log.plain(c.bold(`genex controller ${kind}`));
2045
2369
  log.plain("");
2046
- log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path12.sep)}`);
2370
+ log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path13.sep)}`);
2047
2371
  const plan = [
2048
- ...set.code.map((rel) => ({ from: rel, rel: path12.join(CODE_DEST, rel) })),
2372
+ ...set.code.map((rel) => ({ from: rel, rel: path13.join(CODE_DEST, rel) })),
2049
2373
  ...set.assets.map((rel) => ({
2050
2374
  from: rel,
2051
- rel: path12.join(ASSETS_DEST, path12.basename(rel))
2375
+ rel: path13.join(ASSETS_DEST, path13.basename(rel))
2052
2376
  }))
2053
2377
  ];
2054
2378
  let copied = 0;
2055
2379
  let skipped = 0;
2056
2380
  try {
2057
2381
  for (const file of plan) {
2058
- const dest = path12.join(root, file.rel);
2059
- if (!opts.force && await exists2(dest)) {
2382
+ const dest = path13.join(root, file.rel);
2383
+ if (!opts.force && await exists3(dest)) {
2060
2384
  skipped++;
2061
2385
  log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
2062
2386
  continue;
2063
2387
  }
2064
- await fs11.mkdir(path12.dirname(dest), { recursive: true });
2065
- await fs11.copyFile(path12.join(srcDir, file.from), dest);
2388
+ await fs12.mkdir(path13.dirname(dest), { recursive: true });
2389
+ await fs12.copyFile(path13.join(srcDir, file.from), dest);
2066
2390
  copied++;
2067
2391
  log.dim(` ${file.rel}`);
2068
2392
  }
@@ -2094,8 +2418,8 @@ async function runController(opts) {
2094
2418
  }
2095
2419
  async function installOwnerAvatar(args) {
2096
2420
  const { root, srcDir, apiUrl, token, log } = args;
2097
- const dest = path12.join(root, ASSETS_DEST, "avatar.vrm");
2098
- await fs11.mkdir(path12.dirname(dest), { recursive: true });
2421
+ const dest = path13.join(root, ASSETS_DEST, "avatar.vrm");
2422
+ await fs12.mkdir(path13.dirname(dest), { recursive: true });
2099
2423
  if (token) {
2100
2424
  try {
2101
2425
  const res = await apiFetch(`${apiUrl}/api/avatars/me`, {
@@ -2107,7 +2431,7 @@ async function installOwnerAvatar(args) {
2107
2431
  const vrmRes = await fetch(me.vrmUrl);
2108
2432
  if (vrmRes.ok) {
2109
2433
  const buf = Buffer.from(await vrmRes.arrayBuffer());
2110
- await fs11.writeFile(dest, buf);
2434
+ await fs12.writeFile(dest, buf);
2111
2435
  log.dim(` public/assets/avatar.vrm (your avatar \u2014 ${(buf.length / 1e6).toFixed(1)} MB)`);
2112
2436
  return;
2113
2437
  }
@@ -2118,14 +2442,14 @@ async function installOwnerAvatar(args) {
2118
2442
  log.dim(" avatar fetch failed (offline?); using the bundled default.");
2119
2443
  }
2120
2444
  }
2121
- await fs11.copyFile(path12.join(srcDir, "assets", "default-avatar.vrm"), dest);
2445
+ await fs12.copyFile(path13.join(srcDir, "assets", "default-avatar.vrm"), dest);
2122
2446
  log.dim(
2123
2447
  token ? " public/assets/avatar.vrm (bundled default)" : " public/assets/avatar.vrm (bundled default \u2014 sign in and re-run for your own)"
2124
2448
  );
2125
2449
  }
2126
- async function exists2(p) {
2450
+ async function exists3(p) {
2127
2451
  try {
2128
- await fs11.access(p);
2452
+ await fs12.access(p);
2129
2453
  return true;
2130
2454
  } catch {
2131
2455
  return false;
@@ -2198,7 +2522,7 @@ function rank(items, query) {
2198
2522
  }
2199
2523
 
2200
2524
  // src/index.ts
2201
- var GEN_KINDS = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "texture"]);
2525
+ var GEN_KINDS = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "texture", "image", "video"]);
2202
2526
  var HELP = `${c.bold("genex")} \u2014 set up your ~/.claude workspace, authorize, and publish 3D games.
2203
2527
 
2204
2528
  ${c.bold("Usage")}
@@ -2214,15 +2538,24 @@ ${c.bold("Usage")}
2214
2538
  genex skybox "<prompt>" [options] Generate a skybox (equirect) into public/assets/skybox.
2215
2539
  genex sfx "<prompt>" [options] Generate a sound effect (mp3) into public/assets/sfx.
2216
2540
  genex texture "<prompt>" [options] Generate a PBR texture into public/assets/textures.
2541
+ genex image "<prompt>" [options] Generate an image (PNG); prints a public asset URL.
2542
+ genex video "<prompt>" [options] Generate a video (mp4); prints a public asset URL.
2217
2543
  genex controller <type> [--force] Install a physics controller (character|car|drone)
2218
2544
  into src/controllers (+ assets into public/assets).
2545
+ genex controller anims <sel \u2026> Download extra character animation clips by tag or
2546
+ exact name (sword, stealth, Celebration, \u2026) into
2547
+ public/assets/anims/. --list shows the catalog;
2548
+ --reset forgets the previous selection first.
2219
2549
  genex explore ["<query>"] [options] Search the curated community gallery \u2014 proven
2220
2550
  Three.js systems you can clone or borrow parts
2221
2551
  from. No query lists the whole catalog.
2222
2552
 
2223
- ${c.bold("Options for the generators (`model` `skybox` `sfx` `texture`)")}
2553
+ ${c.bold("Options for the generators (`model` `skybox` `sfx` `texture` `image` `video`)")}
2224
2554
  --terrain (texture) seamless tiling surface for terrain/ground.
2225
- --duration <sec> (sfx) target clip length in seconds.
2555
+ --duration <sec> (sfx, video) target clip length in seconds.
2556
+ --transparent (image) transparent background (alpha) \u2014 for decals/stickers.
2557
+ --aspect <ratio> (image) aspect preset (e.g. square, landscape, portrait).
2558
+ --loop (video) generate a seamless loop.
2226
2559
  --no-wait Enqueue only; don't block waiting for the asset.
2227
2560
  --api-url <url> Override the API base URL.
2228
2561
  --env <path> Token env file (default: ~/.genex/env).
@@ -2300,6 +2633,9 @@ ${c.bold("Examples")}
2300
2633
  genex skybox "golden hour over a misty mountain range"
2301
2634
  genex sfx "punchy laser zap" --duration 2
2302
2635
  genex texture "mossy cracked cobblestone" --terrain
2636
+ genex image "retro arcade poster art" --aspect portrait
2637
+ genex image "neon graffiti tag, spray-paint style" --transparent
2638
+ genex video "swirling neon plasma, seamless loop" --loop
2303
2639
  genex controller character
2304
2640
  genex explore "grass"
2305
2641
  genex explore
@@ -2326,6 +2662,7 @@ function parseArgs(argv) {
2326
2662
  "--categories",
2327
2663
  "--timeout",
2328
2664
  "--duration",
2665
+ "--aspect",
2329
2666
  "--source-repo-url",
2330
2667
  "--source-author",
2331
2668
  "--license",
@@ -2360,9 +2697,21 @@ function parseArgs(argv) {
2360
2697
  case "--terrain":
2361
2698
  parsed.options.terrain = true;
2362
2699
  break;
2700
+ case "--transparent":
2701
+ parsed.options.transparent = true;
2702
+ break;
2703
+ case "--loop":
2704
+ parsed.options.loop = true;
2705
+ break;
2363
2706
  case "--force":
2364
2707
  parsed.options.force = true;
2365
2708
  break;
2709
+ case "--list":
2710
+ parsed.options.list = true;
2711
+ break;
2712
+ case "--reset":
2713
+ parsed.options.reset = true;
2714
+ break;
2366
2715
  case "--regenerate-cover":
2367
2716
  parsed.options.regenerateCover = true;
2368
2717
  break;
@@ -2392,6 +2741,8 @@ function parseArgs(argv) {
2392
2741
  parsed.options.name = arg;
2393
2742
  } else if (parsed.command === "explore") {
2394
2743
  parsed.options.name = `${parsed.options.name} ${arg}`;
2744
+ } else if (parsed.command === "controller") {
2745
+ (parsed.options.selectors ??= []).push(arg);
2395
2746
  } else {
2396
2747
  parsed.error = `Unexpected argument: ${arg}`;
2397
2748
  return parsed;
@@ -2471,6 +2822,9 @@ function applyValueFlag(options, flag, value) {
2471
2822
  options.duration = n;
2472
2823
  break;
2473
2824
  }
2825
+ case "--aspect":
2826
+ options.aspect = value;
2827
+ break;
2474
2828
  }
2475
2829
  }
2476
2830
  async function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "0.36.0",
3
+ "version": "0.38.0",
4
4
  "description": "Set up your ~/.claude workspace, authorize, create a game project, generate AI assets, and publish (genex CLI).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,15 +33,18 @@ Every ported source file carries its own SPDX header
33
33
  (`SPDX-FileCopyrightText: 2023-2026 Erdong Chen`,
34
34
  `SPDX-License-Identifier: MIT`).
35
35
 
36
- ## Quaternius Universal Animation Library — CC0 1.0 Universal
36
+ ## Quaternius Universal Animation Library [Pro] — CC0 1.0 Universal
37
37
 
38
- - `assets/animation-library.glb` — the Universal Animation Library by
39
- **Quaternius** (quaternius.com): 46 humanoid animation clips on a
40
- Blender-Rigify-style skeleton, as bundled by upstream ecctrl.
38
+ - `assets/animation-library.glb` — the bundled 12-clip core, cut from the
39
+ **Universal Animation Library [Pro]** by **Quaternius** (quaternius.com):
40
+ 120 humanoid animation clips on a UE-mannequin-style skeleton.
41
+ - `public/assets/anims/*.glb` — additional per-clip cuts from the same
42
+ library, installed on demand by `npx genex controller anims`.
41
43
 
42
44
  Dedicated to the public domain under the Creative Commons CC0 1.0 Universal
43
45
  license (SPDX: `CC0-1.0`). No attribution is required by the license; this
44
- notice is provided as a courtesy.
46
+ notice is provided as a courtesy — supporting Quaternius on Patreon is a nice
47
+ way to say thanks.
45
48
 
46
49
  ## Default avatar — CC0 1.0 Universal
47
50