@genex-ai/cli-demo 0.35.0 → 0.37.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,6 +13,8 @@ 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/
17
19
  ```
18
20
 
@@ -72,8 +74,8 @@ Defaults: API `https://demo-api.glotech.world`, auth site
72
74
 
73
75
  ## Generating assets
74
76
 
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
77
+ `genex model | skybox | sfx | texture | image | video "<prompt>"` turn a prompt into a
78
+ real, game-ready asset stored in R2. The command blocks until the asset is ready (live SSE
77
79
  stream), then prints its public URL — the game loads it directly, nothing is
78
80
  downloaded or committed:
79
81
 
@@ -83,6 +85,8 @@ downloaded or committed:
83
85
  | `genex skybox "<prompt>"` | Blockade Labs | `…/generations/<id>/skybox-equirect` |
84
86
  | `genex sfx "<prompt>" [--duration <s>]` | ElevenLabs | `…/generations/<id>/audio-sfx` |
85
87
  | `genex texture "<prompt>" [--terrain]` | Gemini | `…/generations/<id>/texture-basecolor` |
88
+ | `genex image "<prompt>" [--transparent] [--aspect <ratio>]` | fal.ai | `…/generations/<id>/image-main` |
89
+ | `genex video "<prompt>" [--duration <s>] [--loop]` | fal.ai | `…/generations/<id>/video-mp4` |
86
90
 
87
91
  Each URL is a permanent `https://assets.genex.technology/...` address served straight
88
92
  from R2 (with CORS, so three.js loads it cross-origin without tainting). It resolves
@@ -92,9 +96,10 @@ Three.js loader code.
92
96
 
93
97
  Auth reuses the existing `GENEX_TOKEN` (run `genex init` first). Server-side, each
94
98
  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.
99
+ `ELEVENLABS_API_KEY`, `GEMINI_API_KEY`, and `FAL_KEY` for image + video); when a key
100
+ is unset that kind falls back to a built-in **mock** provider that returns sample
101
+ assets, so the flow runs keyless. `--no-wait` enqueues without downloading — useful
102
+ for video, which is minutes-class end-to-end.
98
103
 
99
104
  ## Install / run
100
105
 
package/dist/index.js CHANGED
@@ -1655,6 +1655,8 @@ async function runPublish(opts) {
1655
1655
  if (opts.sourceRepoUrl) body.sourceRepoUrl = opts.sourceRepoUrl;
1656
1656
  if (opts.sourceAuthor) body.sourceAuthor = opts.sourceAuthor;
1657
1657
  if (opts.license) body.license = opts.license;
1658
+ if (opts.purchaseUrl) body.purchaseUrl = opts.purchaseUrl;
1659
+ if (opts.productName) body.productName = opts.productName;
1658
1660
  if (detections.embedSdkVersion) body.embedSdkVersion = detections.embedSdkVersion;
1659
1661
  body.multiplayer = detections.multiplayer;
1660
1662
  body.matchmaking = detections.matchmaking ?? null;
@@ -1788,6 +1790,10 @@ async function runGenerate(kind, opts) {
1788
1790
  const options = {};
1789
1791
  if (kind === "texture" && opts.terrain) options.terrain = true;
1790
1792
  if (kind === "sfx" && opts.duration) options.durationSeconds = opts.duration;
1793
+ if (kind === "image" && opts.transparent) options.transparent = true;
1794
+ if (kind === "image" && opts.aspect) options.aspect = opts.aspect;
1795
+ if (kind === "video" && opts.duration) options.durationSeconds = opts.duration;
1796
+ if (kind === "video" && opts.loop) options.loop = true;
1791
1797
  log.plain(c.bold(`genex ${kind}`));
1792
1798
  log.dim(` ${prompt}`);
1793
1799
  log.plain("");
@@ -1824,9 +1830,10 @@ async function runGenerate(kind, opts) {
1824
1830
  }
1825
1831
  log.step("Generating\u2026 (this can take up to a minute)");
1826
1832
  const onProgress = (p) => log.dim(` ${p}%`);
1827
- const deadline = Date.now() + WAIT_TIMEOUT_MS;
1833
+ const timeoutMs = waitTimeoutFor(kind);
1834
+ const deadline = Date.now() + timeoutMs;
1828
1835
  const streamed = await waitViaSSE(apiUrl, token, id, onProgress, deadline);
1829
- const view = streamed === "unsupported" ? await poll(apiUrl, token, id, onProgress) : streamed;
1836
+ const view = streamed === "unsupported" ? await poll(apiUrl, token, id, onProgress, timeoutMs) : streamed;
1830
1837
  if (!view) {
1831
1838
  log.error("Timed out waiting for the generation.");
1832
1839
  process.exitCode = 1;
@@ -1853,6 +1860,11 @@ async function runGenerate(kind, opts) {
1853
1860
  printHint(kind, files, log);
1854
1861
  }
1855
1862
  var WAIT_TIMEOUT_MS = 10 * 60 * 1e3;
1863
+ var KIND_WAIT_TIMEOUT_MS = {
1864
+ video: 15 * 60 * 1e3
1865
+ // 15 min
1866
+ };
1867
+ var waitTimeoutFor = (kind) => KIND_WAIT_TIMEOUT_MS[kind] ?? WAIT_TIMEOUT_MS;
1856
1868
  var TERMINAL = /* @__PURE__ */ new Set(["completed", "failed"]);
1857
1869
  async function waitViaSSE(apiUrl, token, id, onProgress, deadline) {
1858
1870
  let connectFailures = 0;
@@ -1905,8 +1917,8 @@ async function waitViaSSE(apiUrl, token, id, onProgress, deadline) {
1905
1917
  }
1906
1918
  return null;
1907
1919
  }
1908
- async function poll(apiUrl, token, id, onProgress) {
1909
- const deadline = Date.now() + WAIT_TIMEOUT_MS;
1920
+ async function poll(apiUrl, token, id, onProgress, timeoutMs) {
1921
+ const deadline = Date.now() + timeoutMs;
1910
1922
  let last = -1;
1911
1923
  while (Date.now() < deadline) {
1912
1924
  try {
@@ -1935,7 +1947,9 @@ function printHint(kind, files, log) {
1935
1947
  model: `Standard GLB \u2014 load with GLTFLoader straight from the URL \u2014 see the genex-ai-model skill. url = "${url}"`,
1936
1948
  skybox: `Load as an equirectangular texture \u2192 scene.background + scene.environment \u2014 see genex-ai-skybox. url = "${url}"`,
1937
1949
  sfx: `Load with AudioLoader into a THREE.PositionalAudio (camera needs an AudioListener) \u2014 see genex-ai-sfx. url = "${url}"`,
1938
- texture: `Load each map with TextureLoader (RepeatWrapping) into a MeshStandardMaterial \u2014 see genex-ai-texture. Use the URLs above by role.`
1950
+ texture: `Load each map with TextureLoader (RepeatWrapping) into a MeshStandardMaterial \u2014 see genex-ai-texture. Use the URLs above by role.`,
1951
+ image: `Load with TextureLoader (set colorSpace = SRGBColorSpace) onto any mesh/plane/sprite \u2014 see genex-ai-image. url = "${url}"`,
1952
+ video: `Wire an HTMLVideoElement (crossOrigin="anonymous", muted, loop, playsInline) into a THREE.VideoTexture \u2014 see genex-ai-video. url = "${url}"`
1939
1953
  };
1940
1954
  log.dim(` ${hint[kind]}`);
1941
1955
  log.dim(" Reference the URL directly in your code \u2014 don't download it into the repo.");
@@ -2196,7 +2210,7 @@ function rank(items, query) {
2196
2210
  }
2197
2211
 
2198
2212
  // src/index.ts
2199
- var GEN_KINDS = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "texture"]);
2213
+ var GEN_KINDS = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "texture", "image", "video"]);
2200
2214
  var HELP = `${c.bold("genex")} \u2014 set up your ~/.claude workspace, authorize, and publish 3D games.
2201
2215
 
2202
2216
  ${c.bold("Usage")}
@@ -2212,15 +2226,20 @@ ${c.bold("Usage")}
2212
2226
  genex skybox "<prompt>" [options] Generate a skybox (equirect) into public/assets/skybox.
2213
2227
  genex sfx "<prompt>" [options] Generate a sound effect (mp3) into public/assets/sfx.
2214
2228
  genex texture "<prompt>" [options] Generate a PBR texture into public/assets/textures.
2229
+ genex image "<prompt>" [options] Generate an image (PNG); prints a public asset URL.
2230
+ genex video "<prompt>" [options] Generate a video (mp4); prints a public asset URL.
2215
2231
  genex controller <type> [--force] Install a physics controller (character|car|drone)
2216
2232
  into src/controllers (+ assets into public/assets).
2217
2233
  genex explore ["<query>"] [options] Search the curated community gallery \u2014 proven
2218
2234
  Three.js systems you can clone or borrow parts
2219
2235
  from. No query lists the whole catalog.
2220
2236
 
2221
- ${c.bold("Options for the generators (`model` `skybox` `sfx` `texture`)")}
2237
+ ${c.bold("Options for the generators (`model` `skybox` `sfx` `texture` `image` `video`)")}
2222
2238
  --terrain (texture) seamless tiling surface for terrain/ground.
2223
- --duration <sec> (sfx) target clip length in seconds.
2239
+ --duration <sec> (sfx, video) target clip length in seconds.
2240
+ --transparent (image) transparent background (alpha) \u2014 for decals/stickers.
2241
+ --aspect <ratio> (image) aspect preset (e.g. square, landscape, portrait).
2242
+ --loop (video) generate a seamless loop.
2224
2243
  --no-wait Enqueue only; don't block waiting for the asset.
2225
2244
  --api-url <url> Override the API base URL.
2226
2245
  --env <path> Token env file (default: ~/.genex/env).
@@ -2263,6 +2282,9 @@ ${c.bold("Options for `preview` / `publish`")}
2263
2282
  shown as "Ported from \u2026" credit on the game page.
2264
2283
  --source-author <name> (publish) Upstream author to credit.
2265
2284
  --license <label> (publish) Upstream license label (e.g. MIT).
2285
+ --purchase-url <url> (operator) External purchase URL \u2014 marks the game a
2286
+ paid-product demo ("Get \u2026" replaces Remix). Admin-only.
2287
+ --product-name <name> (operator) Product name for the "Get {name}" button.
2266
2288
  --api-url <url> (publish) Override the API base URL.
2267
2289
  --env <path> Token env file (default: ~/.genex/env).
2268
2290
 
@@ -2295,6 +2317,9 @@ ${c.bold("Examples")}
2295
2317
  genex skybox "golden hour over a misty mountain range"
2296
2318
  genex sfx "punchy laser zap" --duration 2
2297
2319
  genex texture "mossy cracked cobblestone" --terrain
2320
+ genex image "retro arcade poster art" --aspect portrait
2321
+ genex image "neon graffiti tag, spray-paint style" --transparent
2322
+ genex video "swirling neon plasma, seamless loop" --loop
2298
2323
  genex controller character
2299
2324
  genex explore "grass"
2300
2325
  genex explore
@@ -2321,9 +2346,12 @@ function parseArgs(argv) {
2321
2346
  "--categories",
2322
2347
  "--timeout",
2323
2348
  "--duration",
2349
+ "--aspect",
2324
2350
  "--source-repo-url",
2325
2351
  "--source-author",
2326
- "--license"
2352
+ "--license",
2353
+ "--purchase-url",
2354
+ "--product-name"
2327
2355
  ]);
2328
2356
  let i = 0;
2329
2357
  while (i < argv.length) {
@@ -2353,6 +2381,12 @@ function parseArgs(argv) {
2353
2381
  case "--terrain":
2354
2382
  parsed.options.terrain = true;
2355
2383
  break;
2384
+ case "--transparent":
2385
+ parsed.options.transparent = true;
2386
+ break;
2387
+ case "--loop":
2388
+ parsed.options.loop = true;
2389
+ break;
2356
2390
  case "--force":
2357
2391
  parsed.options.force = true;
2358
2392
  break;
@@ -2442,6 +2476,12 @@ function applyValueFlag(options, flag, value) {
2442
2476
  case "--license":
2443
2477
  options.license = value;
2444
2478
  break;
2479
+ case "--purchase-url":
2480
+ options.purchaseUrl = value;
2481
+ break;
2482
+ case "--product-name":
2483
+ options.productName = value;
2484
+ break;
2445
2485
  case "--timeout": {
2446
2486
  const n = Number(value);
2447
2487
  if (!Number.isFinite(n) || n <= 0) {
@@ -2458,6 +2498,9 @@ function applyValueFlag(options, flag, value) {
2458
2498
  options.duration = n;
2459
2499
  break;
2460
2500
  }
2501
+ case "--aspect":
2502
+ options.aspect = value;
2503
+ break;
2461
2504
  }
2462
2505
  }
2463
2506
  async function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "0.35.0",
3
+ "version": "0.37.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": {
@@ -0,0 +1,181 @@
1
+ ---
2
+ name: genex-ai-image
3
+ description: Generate a real image (PNG/JPEG) from a text prompt with `npx genex image`, then load it into Three.js on any mesh, plane, or sprite. Use for posters, paintings, billboards, signs, logos, sprites, card/item art, loading screens, textures for in-game screens, and decals/stickers ("wanted poster", "arcade cabinet marquee", "neon graffiti tag") rather than a procedural/shader look. Pass `--transparent` for anything with an alpha channel.
4
+ ---
5
+
6
+ # Genex AI · Image
7
+
8
+ Turn a prompt into a real raster image and put it anywhere in the game — a poster,
9
+ a painting, a billboard, a sign, a logo, a sprite, card/item art, a loading screen,
10
+ art on an in-game screen, or a decal/sticker.
11
+
12
+ ## When to use this vs. procedural materials
13
+
14
+ - **Use `npx genex image`** for a specific, recognizable picture you can describe —
15
+ "vintage travel poster of Mars", "guild crest with crossed swords", "arcade
16
+ marquee art". You get a real image.
17
+ - **Use `$genex-threejs-procedural-materials`** for stylized/abstract or fully
18
+ parametric surfaces authored in shaders. Use `$genex-ai-texture` for a *tiling*
19
+ PBR surface (floors, ground, walls) — this skill is for a single flat picture.
20
+
21
+ ## Run
22
+
23
+ ```bash
24
+ npx genex image "<prompt>"
25
+ npx genex image "neon graffiti tag, spray-paint style" --transparent # PNG with alpha (decals/stickers/logos)
26
+ ```
27
+
28
+ Blocks until ready, then prints its public URL:
29
+
30
+ ```
31
+ https://assets.genex.technology/generations/<id>/image-main
32
+ ```
33
+
34
+ The image lives in Genex storage (R2) and loads straight from that URL — you don't
35
+ download it and nothing is committed to your repo. The URL is permanent (local dev,
36
+ published game, and remixes alike).
37
+
38
+ ## Load it into the scene
39
+
40
+ Load the image and apply it to any mesh, plane, or sprite:
41
+
42
+ ```ts
43
+ import * as THREE from "three";
44
+
45
+ // the URL `npx genex image` printed (R2 sends CORS headers, so cross-origin works):
46
+ const IMAGE_URL = "https://assets.genex.technology/generations/<id>/image-main";
47
+ const map = await new THREE.TextureLoader().loadAsync(IMAGE_URL);
48
+ map.colorSpace = THREE.SRGBColorSpace; // pictures are sRGB — without this they look washed/dark
49
+ map.anisotropy = renderer.capabilities.getMaxAnisotropy(); // stays sharp at grazing angles
50
+
51
+ // a poster/painting/sign on a wall — a flat plane:
52
+ const poster = new THREE.Mesh(
53
+ new THREE.PlaneGeometry(2, 3), // match the image aspect (w:h)
54
+ new THREE.MeshStandardMaterial({ map, roughness: 0.9, metalness: 0 }),
55
+ );
56
+ scene.add(poster);
57
+ ```
58
+
59
+ For a `--transparent` PNG, set `transparent: true` on the material so the alpha shows.
60
+ For a screen-space sprite (HUD art, an icon) use `new THREE.Sprite(new THREE.SpriteMaterial({ map }))`.
61
+ For art that must glow (a lit sign, a screen) use `MeshBasicMaterial` (unlit) so scene lighting doesn't darken it.
62
+
63
+ ## Decals & stickers
64
+
65
+ **Anything applied *on top* of a surface — a decal, sticker, spray tag, logo, or
66
+ bullet hole — needs an alpha channel, so always generate it with `--transparent`.**
67
+ Without alpha you get an opaque rectangle instead of a shaped mark.
68
+
69
+ Project it onto the target mesh with `DecalGeometry` (the canonical spray-paint look —
70
+ clips to the surface and wraps around corners):
71
+
72
+ ```ts
73
+ import * as THREE from "three";
74
+ import { DecalGeometry } from "three/addons/geometries/DecalGeometry.js";
75
+
76
+ // ONE shared texture + material for all sprays (generated with --transparent):
77
+ const map = await new THREE.TextureLoader().loadAsync(IMAGE_URL);
78
+ map.colorSpace = THREE.SRGBColorSpace;
79
+ map.anisotropy = renderer.capabilities.getMaxAnisotropy();
80
+ const sprayMat = new THREE.MeshStandardMaterial({
81
+ map, transparent: true, depthTest: true, depthWrite: false,
82
+ polygonOffset: true, polygonOffsetFactor: -4, // pulls the decal forward — kills z-fighting
83
+ });
84
+
85
+ const raycaster = new THREE.Raycaster();
86
+ const helper = new THREE.Object3D(); // orientation scratch — never added to the scene
87
+ const decals: THREE.Mesh[] = [];
88
+ const MAX_DECALS = 20;
89
+
90
+ function spray(): void {
91
+ raycaster.setFromCamera(new THREE.Vector2(0, 0), camera); // screen centre
92
+ const hit = raycaster.intersectObjects(sprayables, false)[0]; // sprayables = your wall meshes
93
+ if (!hit || !hit.face) return;
94
+ const n = hit.face.normal.clone() // face.normal is LOCAL space —
95
+ .applyNormalMatrix(new THREE.Matrix3().getNormalMatrix(hit.object.matrixWorld)); // must transform it
96
+ helper.position.copy(hit.point);
97
+ helper.lookAt(hit.point.clone().add(n));
98
+ const S = 1; // metres; for non-square art size.y = S * imgH/imgW
99
+ const geom = new DecalGeometry(hit.object as THREE.Mesh, hit.point,
100
+ helper.rotation.clone(), new THREE.Vector3(S, S, S * 0.5)); // size.z = wrap depth
101
+ const decal = new THREE.Mesh(geom, sprayMat);
102
+ decal.renderOrder = 100 + decals.length; // newer sprays draw on top
103
+ (hit.object as THREE.Mesh).attach(decal); // verts are world-space; attach keeps them correct
104
+ decals.push(decal);
105
+ if (decals.length > MAX_DECALS) { // FIFO cap — dispose the oldest
106
+ const old = decals.shift()!;
107
+ old.removeFromParent();
108
+ old.geometry.dispose(); // geometry is per-spray — MUST dispose; the shared material/map are never disposed here
109
+ }
110
+ }
111
+
112
+ window.addEventListener("keydown", (e) => {
113
+ if (e.code === "KeyT" && !e.repeat) spray();
114
+ });
115
+ ```
116
+
117
+ Gotchas (all bite in practice):
118
+
119
+ - **`hit.face.normal` is LOCAL space** — apply the normal-matrix from `matrixWorld`.
120
+ Skipping it "works by accident" only on an unrotated, unscaled wall; any rotated
121
+ wall gets the decal facing the wrong way.
122
+ - **`depthWrite: false` means overlap order is draw order** — increment `renderOrder`
123
+ per spray, or stacked decals flicker and sort wrongly.
124
+ - **One decal clips against ONE mesh** — a spray straddling two wall meshes gets cut
125
+ at the boundary. Pass the single `hit.object`, or make walls separate meshes.
126
+ - **`size.z` too large on a thin wall wraps the decal onto the back face** (visible
127
+ from behind). Keep `size.z` below the wall thickness.
128
+ - **`DecalGeometry` is static-mesh only** — no `SkinnedMesh`/morph targets. For a
129
+ moving or skinned target, use a small `PlaneGeometry` quad offset along the normal
130
+ (`hit.point + n * 0.01`, `quad.lookAt(hit.point.clone().add(n))`) instead.
131
+
132
+ ## Multiplayer
133
+
134
+ The asset URL is public, permanent, and CORS-open, so it is safe to broadcast the
135
+ string to every player. A placed-at-runtime mark (a decal included) is static shared
136
+ world state — put it on `room.shared`, **not** `objects` (no movement to smooth) and
137
+ **not** `send` (late joiners would see a bare wall; `shared` keys replay on connect).
138
+
139
+ Use a **fixed ring of slots** and overwrite the oldest — **never a fresh key per
140
+ spray**. The relay caps game-writable `shared` keys at **256 per room, keys are
141
+ permanent and undeletable, and new keys past the cap are silently dropped forever**,
142
+ so a new key per spray eventually breaks the game:
143
+
144
+ ```ts
145
+ let next = 0;
146
+ const RING = 32; // decal:0 .. decal:31
147
+ function placeShared(url: string, hit: { point: THREE.Vector3; normal: THREE.Vector3 }) {
148
+ room.shared.set(`decal:${next % RING}`, { url, p: hit.point.toArray(), n: hit.normal.toArray() });
149
+ next++;
150
+ }
151
+ room.on("shared", (key, value) => { // every player (incl. late joiners) rebuilds the decal
152
+ if (key.startsWith("decal:") && value) spawnDecalFromShared(value);
153
+ });
154
+ ```
155
+
156
+ See `$genex-threejs-multiplayer` for the `shared` channel rules and the room API.
157
+
158
+ ## Publish checklist
159
+
160
+ - Load it from the **URL** the command printed — absolute and permanent, so it resolves
161
+ the same in local dev, the published game, and remixes. Nothing to commit.
162
+ - Don't copy the image into `public/assets/` — generated assets live in R2, not the repo.
163
+
164
+ ## Options
165
+
166
+ - `--transparent` — PNG with an alpha channel (mandatory for decals/stickers/logos —
167
+ anything laid on top of a surface).
168
+ - `--aspect <ratio>` — image shape (e.g. `square`, `16:9`, `9:16`); default is square.
169
+ - `--no-wait` — enqueue and return immediately (the file won't be downloaded;
170
+ re-run without `--no-wait` to fetch it).
171
+ - `--api-url <url>` — override the API base (local dev).
172
+
173
+ ## Troubleshooting
174
+
175
+ - **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it writes your `GENEX_TOKEN`).
176
+ - **"Prompt rejected"** — the provider's content-safety filter blocked the prompt.
177
+ This is non-retryable; retrying the same wording fails again. Rewrite the prompt.
178
+ - **Decal is an opaque rectangle** — the image has no alpha. Regenerate with
179
+ `--transparent`.
180
+ - **Colors look washed/dark** — ensure `map.colorSpace = THREE.SRGBColorSpace`.
181
+ - **Decal blurs at oblique angles** — set `map.anisotropy = renderer.capabilities.getMaxAnisotropy()`.
@@ -0,0 +1,148 @@
1
+ ---
2
+ name: genex-ai-video
3
+ description: Generate a real video clip (H.264 mp4) from a text prompt with `npx genex video`, then play it in Three.js on any surface via VideoTexture. Use for in-game TVs/screens/monitors, animated billboards, cutscene clips, ambient backdrops, portals, and video decals ("news broadcast on a TV", "swirling portal", "animated arcade attract screen") rather than a static image or shader effect. Pass `--loop` for a seamless loop.
4
+ ---
5
+
6
+ # Genex AI · Video
7
+
8
+ Turn a prompt into a real mp4 clip and play it anywhere in the game — an in-game
9
+ TV/screen/monitor, an animated billboard, a cutscene, an ambient backdrop, a portal,
10
+ or a video decal.
11
+
12
+ ## When to use this vs. a static image or shader
13
+
14
+ - **Use `npx genex video`** for moving footage you can describe — "static-y CRT
15
+ news broadcast", "swirling neon portal", "rain running down glass". You get a real
16
+ mp4.
17
+ - **Use `$genex-ai-image`** for a single still picture, or
18
+ `$genex-threejs-procedural-vfx` for parametric real-time effects (particles,
19
+ trails, shockwaves) authored in code.
20
+
21
+ ## Run
22
+
23
+ ```bash
24
+ npx genex video "<prompt>"
25
+ npx genex video "swirling neon plasma, seamless loop" --loop # seamless loop for screens/backdrops
26
+ ```
27
+
28
+ Video takes a **minute or two** end to end (queue + generation). If you're building
29
+ other things meanwhile, add `--no-wait` to enqueue and come back for it. Blocks until
30
+ ready, then prints its public URL:
31
+
32
+ ```
33
+ https://assets.genex.technology/generations/<id>/video-mp4
34
+ ```
35
+
36
+ The clip lives in Genex storage (R2) and loads straight from that URL — you don't
37
+ download it and nothing is committed to your repo. The URL is permanent (local dev,
38
+ published game, and remixes alike).
39
+
40
+ > **Cost & length:** the default is a **5-second, 720p** clip, and that IS the right
41
+ > default for a looping screen or backdrop — only pass `--duration` when the content
42
+ > genuinely needs to be longer (a cutscene). Longer, higher-res clips cost more and
43
+ > take longer. mp4 has **no alpha channel**, so a video is always a full rectangle
44
+ > (there are no transparent video decals).
45
+
46
+ ## Play it in Three.js
47
+
48
+ Video needs an `HTMLVideoElement`, and browsers block autoplay — the first
49
+ `video.play()` must run inside a user gesture (any click or keypress). Wrap it in a
50
+ `VideoTexture` and put it on any surface:
51
+
52
+ ```ts
53
+ import * as THREE from "three";
54
+
55
+ // the URL `npx genex video` printed (R2 sends CORS headers, so cross-origin works):
56
+ const VIDEO_URL = "https://assets.genex.technology/generations/<id>/video-mp4";
57
+ const video = document.createElement("video");
58
+ video.src = VIDEO_URL;
59
+ video.crossOrigin = "anonymous"; // REQUIRED to upload cross-origin video to WebGL (else a tainted-source error)
60
+ video.muted = true; // muted is what lets it play under autoplay policy
61
+ video.loop = true;
62
+ video.playsInline = true; // iOS: no fullscreen takeover
63
+
64
+ const texture = new THREE.VideoTexture(video); // auto-updates every frame — no per-frame code
65
+ texture.colorSpace = THREE.SRGBColorSpace;
66
+
67
+ // an in-game TV / animated billboard — MeshBasicMaterial so it glows regardless of scene lighting:
68
+ const screen = new THREE.Mesh(
69
+ new THREE.PlaneGeometry(3.2, 1.8), // 16:9
70
+ new THREE.MeshBasicMaterial({ map: texture }),
71
+ );
72
+ scene.add(screen);
73
+
74
+ // start playback on a user gesture (never swallow the rejection):
75
+ window.addEventListener("keydown", () => {
76
+ video.play().catch((e) => console.warn("video play", e));
77
+ }, { once: true });
78
+ ```
79
+
80
+ One `VideoTexture` can feed **many** surfaces — a bank of monitors, N sprays. Cost is
81
+ per distinct `<video>` element (media decode + one GPU upload per frame), **not** per
82
+ surface, so sharing one element for every screen is nearly free (all play in sync).
83
+ Keep clips **≤720p**. Pause the element (`video.pause()`) when no video surface is
84
+ visible.
85
+
86
+ ## Video decals
87
+
88
+ A video decal is the same `DecalGeometry` projection as an image decal — read the
89
+ **Decals & stickers** section of `$genex-ai-image` for the full recipe and gotchas —
90
+ with one change: swap the `TextureLoader` map for the `VideoTexture` above, and call
91
+ `video.play().catch(...)` inside the spray handler (the keypress is the user gesture).
92
+ All decals can share the one `VideoTexture`. Because mp4 has no alpha, a video decal
93
+ is a full rectangle (fine for a screen-shaped mark; use `$genex-ai-image --transparent`
94
+ for a shaped sticker). **Layering gotcha:** an opaque video decal draws in the opaque
95
+ pass — *before* any `transparent: true` image decal — so image decals always land on
96
+ top regardless of spray order or `renderOrder`. If you mix both and need strict
97
+ newest-on-top, set `transparent: true` on the video decal material too.
98
+
99
+ ## Multiplayer
100
+
101
+ The asset URL is public, permanent, and CORS-open, so it is safe to broadcast the
102
+ string to every player. A placed-at-runtime surface (a video decal included) is static
103
+ shared world state — put it on `room.shared`, **not** `objects` (no movement to smooth)
104
+ and **not** `send` (late joiners would see a bare wall; `shared` keys replay on connect).
105
+
106
+ Use a **fixed ring of slots** and overwrite the oldest — **never a fresh key per
107
+ placement**. The relay caps game-writable `shared` keys at **256 per room, keys are
108
+ permanent and undeletable, and new keys past the cap are silently dropped forever**:
109
+
110
+ ```ts
111
+ let next = 0;
112
+ const RING = 32; // decal:0 .. decal:31
113
+ function placeShared(url: string, hit: { point: THREE.Vector3; normal: THREE.Vector3 }) {
114
+ room.shared.set(`decal:${next % RING}`, { url, p: hit.point.toArray(), n: hit.normal.toArray() });
115
+ next++;
116
+ }
117
+ room.on("shared", (key, value) => { // every player (incl. late joiners) rebuilds it
118
+ if (key.startsWith("decal:") && value) spawnVideoDecalFromShared(value);
119
+ });
120
+ ```
121
+
122
+ See `$genex-threejs-multiplayer` for the `shared` channel rules and the room API.
123
+
124
+ ## Publish checklist
125
+
126
+ - Load it from the **URL** the command printed — absolute and permanent, so it resolves
127
+ the same in local dev, the published game, and remixes. Nothing to commit.
128
+ - Don't copy the mp4 into `public/assets/` — generated assets live in R2, not the repo.
129
+
130
+ ## Options
131
+
132
+ - `--loop` — a seamless loop (for screens, ambient backdrops, video decals).
133
+ - `--duration <sec>` — clip length 1–15; default 5. Only raise it when the content
134
+ genuinely needs more — longer clips cost more and take longer.
135
+ - `--no-wait` — enqueue and return immediately (the file won't be downloaded;
136
+ re-run without `--no-wait` to fetch it). Handy for video since it takes a minute or two.
137
+ - `--api-url <url>` — override the API base (local dev).
138
+
139
+ ## Troubleshooting
140
+
141
+ - **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it writes your `GENEX_TOKEN`).
142
+ - **"Prompt rejected"** — the provider's content-safety filter blocked the prompt.
143
+ This is non-retryable; retrying the same wording fails again. Rewrite the prompt.
144
+ - **Nothing plays / black surface** — the first `video.play()` must run inside a user
145
+ gesture (click/keydown); confirm it's called and its promise rejection is logged.
146
+ - **Tainted-source / security error** — set `video.crossOrigin = "anonymous"` before
147
+ `video.src`.
148
+ - **Colors look washed/dark** — ensure `texture.colorSpace = THREE.SRGBColorSpace`.
@@ -37,13 +37,16 @@ npx genex model "weathered wooden barrel" # a 3D mesh (GLB)
37
37
  npx genex skybox "golden hour over mountains" # a 360° sky + lighting
38
38
  npx genex sfx "punchy laser zap" --duration 2 # a sound effect (mp3)
39
39
  npx genex texture "mossy cobblestone" --terrain # a tiling surface texture
40
+ npx genex image "vintage travel poster" # a picture (poster/sign/sprite/decal)
41
+ npx genex video "swirling neon portal" --loop # a video clip (screen/backdrop)
40
42
  ```
41
43
 
42
44
  (Run them inside your project — the `@genex-ai/cli-demo` dev dependency makes
43
45
  `npx genex` resolve to the right CLI.)
44
46
 
45
47
  Each has a focused skill with the exact loader code — `$genex-ai-model`,
46
- `$genex-ai-skybox`, `$genex-ai-sfx`, `$genex-ai-texture`.
48
+ `$genex-ai-skybox`, `$genex-ai-sfx`, `$genex-ai-texture`, `$genex-ai-image`,
49
+ `$genex-ai-video`.
47
50
 
48
51
  ## Identity & saves (every game)
49
52
 
@@ -60,13 +60,12 @@ recipes (sports/ball, shooter, co-op).
60
60
  ## Real (AI-generated) assets — `npx genex` commands
61
61
 
62
62
  When the user wants a **specific, recognizable asset** (a named object, a described
63
- sky, a particular sound or surface) rather than something authored in code, generate
64
- it with an `npx genex` command (run inside the project, where the `@genex-ai/cli-demo`
65
- dev dependency makes `genex` resolve to the right CLI). Each drops a real file into
66
- `public/assets/<kind>/` so it ships with the built game load it at runtime as
67
- `./assets/...` (the `public/` prefix is stripped when served; a bare `./assets/` dir
68
- would be dropped from the production build). Each skill has the exact Three.js
69
- loader code.
63
+ sky, a particular sound, surface, image, or clip) rather than something authored in
64
+ code, generate it with an `npx genex` command (run inside the project, where the
65
+ `@genex-ai/cli-demo` dev dependency makes `genex` resolve to the right CLI). Each
66
+ prints a permanent public `assets.genex.technology` URL you load straight from at
67
+ runtime the asset lives in Genex storage (R2), not your repo, so there's nothing to
68
+ commit. Each skill has the exact Three.js loader code.
70
69
 
71
70
  | Work needed | Generate with | Skill |
72
71
  | --- | --- | --- |
@@ -74,6 +73,8 @@ loader code.
74
73
  | a described 360° sky / backdrop + image-based lighting | `npx genex skybox "<prompt>"` | `$genex-ai-skybox` |
75
74
  | a specific sound effect tied to an event | `npx genex sfx "<prompt>"` | `$genex-ai-sfx` |
76
75
  | a photoreal surface/material on a mesh or terrain | `npx genex texture "<prompt>" [--terrain]` | `$genex-ai-texture` |
76
+ | a picture on a plane/sprite — poster, sign, sprite, card art, decal/sticker | `npx genex image "<prompt>" [--transparent]` | `$genex-ai-image` |
77
+ | a video clip on a surface — in-game screen, billboard, cutscene, backdrop | `npx genex video "<prompt>" [--loop]` | `$genex-ai-video` |
77
78
 
78
79
  Prefer the **procedural** skills above for abstract/parametric/animated systems
79
80
  (geometry, materials, sky, water, VFX) — no files, infinite variation. Prefer the