@genex-ai/cli-demo 1.30.0-dev.645 → 1.31.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 +0 -18
- package/dist/index.js +2839 -4768
- package/package.json +3 -3
- package/templates/controllers/character/follow-camera.ts +1 -16
- package/templates/controllers/character/meshy/meshy-loader.ts +2 -3
- package/templates/controllers/quality/pick-asset.ts +16 -49
- package/templates/controllers/shared/physics-world.ts +4 -6
- package/templates/skills/genex-ai-character/SKILL.md +15 -77
- package/templates/skills/genex-ai-menu/SKILL.md +11 -15
- package/templates/skills/genex-ai-model/SKILL.md +7 -45
- package/templates/skills/genex-ai-texture/SKILL.md +1 -1
- package/templates/skills/genex-ai-video/SKILL.md +17 -75
- package/templates/skills/genex-game-director/SKILL.md +46 -112
- package/templates/skills/genex-game-director/references/design-contract.md +5 -13
- package/templates/skills/genex-game-director/references/routing-map.md +45 -30
- package/templates/skills/genex-getting-started/SKILL.md +2 -2
- package/templates/skills/genex-monetization/SKILL.md +171 -0
- package/templates/skills/genex-threejs-adaptive-quality/SKILL.md +0 -21
- package/templates/skills/genex-threejs-character-controller/SKILL.md +5 -17
- package/templates/skills/genex-threejs-creatures/SKILL.md +1 -8
- package/templates/skills/genex-threejs-embed-auth/SKILL.md +52 -8
- package/templates/skills/genex-threejs-game-ui/SKILL.md +6 -55
- package/templates/skills/genex-threejs-procedural-assets/SKILL.md +10 -17
- package/templates/skills/genex-threejs-visual-validation/SKILL.md +2 -12
- package/templates/skills/genex-tool-audio/SKILL.md +2 -3
- package/templates/skills/genex-tool-character/SKILL.md +5 -36
- package/templates/skills/genex-tool-image/SKILL.md +2 -4
- package/templates/skills/genex-tool-model/SKILL.md +6 -32
- package/templates/skills/genex-tool-texture/SKILL.md +1 -1
- package/templates/skills/genex-tool-video/SKILL.md +5 -28
- package/templates/skills/genex-tool-workflow/SKILL.md +1 -4
- package/templates/skills/genex-updates/SKILL.md +1 -1
- package/dist/blender-mcp-Q6PSFYSE.js +0 -241
- package/dist/blender-serve-BF4FZ55Z.js +0 -244
- package/dist/chunk-2COG4P3T.js +0 -968
- package/dist/chunk-HYCSNWYX.js +0 -126
- package/templates/blender-service/demo/castle.py +0 -117
- package/templates/blender-service/gpu_witness.py +0 -245
- package/templates/blender-service/ops.py +0 -225
- package/templates/blender-service/pool.py +0 -910
- package/templates/blender-service/server.py +0 -611
- package/templates/blender-service/supervisor.py +0 -221
- package/templates/blender-service/views.py +0 -281
- package/templates/controllers/quality/deadline.ts +0 -117
- package/templates/skills/genex-blender-scene/SKILL.md +0 -243
- package/templates/skills/genex-lane-card/SKILL.md +0 -78
- package/templates/skills/genex-tool-publish/SKILL.md +0 -100
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
"""Scene operations. Every mutating call runs on the HTTP server's own thread,
|
|
2
|
-
which is Blender's main thread -- bpy is not thread-safe and the server is
|
|
3
|
-
deliberately non-threading so this holds without a queue."""
|
|
4
|
-
|
|
5
|
-
import contextlib
|
|
6
|
-
import io
|
|
7
|
-
import os
|
|
8
|
-
import tempfile
|
|
9
|
-
import traceback
|
|
10
|
-
import urllib.parse
|
|
11
|
-
import urllib.request
|
|
12
|
-
|
|
13
|
-
import bpy
|
|
14
|
-
|
|
15
|
-
from views import scene_bounds
|
|
16
|
-
|
|
17
|
-
# The exec namespace PERSISTS across calls, so an agent can define a helper in
|
|
18
|
-
# one step and use it in the next. That is the difference between a REPL and a
|
|
19
|
-
# series of unrelated scripts.
|
|
20
|
-
NAMESPACE = {}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def reset_namespace():
|
|
24
|
-
import math
|
|
25
|
-
import random
|
|
26
|
-
|
|
27
|
-
import bmesh
|
|
28
|
-
import mathutils
|
|
29
|
-
|
|
30
|
-
NAMESPACE.clear()
|
|
31
|
-
NAMESPACE.update(
|
|
32
|
-
{"bpy": bpy, "bmesh": bmesh, "mathutils": mathutils, "math": math, "random": random}
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def run_script(source):
|
|
37
|
-
"""Execute arbitrary Python against the live scene.
|
|
38
|
-
|
|
39
|
-
Arbitrary by design: a fixed tool vocabulary cannot express 'build a castle'.
|
|
40
|
-
The containment story is the container -- disposable, egress-locked in the
|
|
41
|
-
hosted path -- never a sanitizer on this string, which does not work.
|
|
42
|
-
"""
|
|
43
|
-
out, err = io.StringIO(), io.StringIO()
|
|
44
|
-
error = None
|
|
45
|
-
with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
|
|
46
|
-
try:
|
|
47
|
-
exec(compile(source, "<genex-exec>", "exec"), NAMESPACE)
|
|
48
|
-
except Exception:
|
|
49
|
-
error = traceback.format_exc(limit=8)
|
|
50
|
-
return {"stdout": out.getvalue(), "stderr": err.getvalue(), "error": error}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def _tri_count(obj):
|
|
54
|
-
if obj.type != "MESH" or obj.data is None:
|
|
55
|
-
return 0
|
|
56
|
-
return sum(max(len(p.vertices) - 2, 0) for p in obj.data.polygons)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def scene_info():
|
|
60
|
-
"""The numeric truths. These are the GATE -- the picture is for steering.
|
|
61
|
-
|
|
62
|
-
A screenshot judge is measurably bad at pass/fail (WorldCoder-Bench: a
|
|
63
|
-
screenshot+VLM gate passes 2.7% of outputs), so an agent should assert on
|
|
64
|
-
these counts and use the render to decide what to change.
|
|
65
|
-
"""
|
|
66
|
-
objects, total_tris = [], 0
|
|
67
|
-
for obj in bpy.context.scene.objects:
|
|
68
|
-
tris = _tri_count(obj)
|
|
69
|
-
total_tris += tris
|
|
70
|
-
objects.append(
|
|
71
|
-
{
|
|
72
|
-
"name": obj.name,
|
|
73
|
-
"type": obj.type,
|
|
74
|
-
"tris": tris,
|
|
75
|
-
"location": [round(v, 4) for v in obj.location],
|
|
76
|
-
"dimensions": [round(v, 4) for v in obj.dimensions],
|
|
77
|
-
"materials": [m.name for m in obj.data.materials if m]
|
|
78
|
-
if getattr(obj.data, "materials", None)
|
|
79
|
-
else [],
|
|
80
|
-
"visible": obj.visible_get(),
|
|
81
|
-
}
|
|
82
|
-
)
|
|
83
|
-
center, radius = scene_bounds()
|
|
84
|
-
return {
|
|
85
|
-
"objectCount": len(objects),
|
|
86
|
-
"meshCount": sum(1 for o in objects if o["type"] == "MESH"),
|
|
87
|
-
"totalTris": total_tris,
|
|
88
|
-
"materialCount": len(bpy.data.materials),
|
|
89
|
-
"bounds": {
|
|
90
|
-
"center": [round(v, 4) for v in center],
|
|
91
|
-
"radius": round(radius, 4),
|
|
92
|
-
},
|
|
93
|
-
"objects": objects,
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def reset_scene():
|
|
98
|
-
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
99
|
-
reset_namespace()
|
|
100
|
-
return scene_info()
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
# A picture the agent LOOKS AT goes inline; an artifact the GAME SHIPS goes to
|
|
104
|
-
# object storage. Base64 inflates 4/3 plus JSON escaping, so ~7 MB of raw GLB
|
|
105
|
-
# already blows RunPod's 10 MB /run ceiling -- and it fails only on long,
|
|
106
|
-
# successful sessions that generated a lot of texture, which is the worst time.
|
|
107
|
-
INLINE_GLB_MAX_BYTES = 6 * 1024 * 1024
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def export_glb(path):
|
|
111
|
-
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
|
|
112
|
-
bpy.ops.export_scene.gltf(filepath=path, export_format="GLB", use_selection=False)
|
|
113
|
-
return {"path": path, "bytes": os.path.getsize(path)}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def upload_file(path, url, headers=None):
|
|
117
|
-
"""PUT the bytes to a presigned URL. The service never holds an account key
|
|
118
|
-
-- the caller mints a single-object, short-TTL URL, so code running inside
|
|
119
|
-
/exec has nothing durable to steal."""
|
|
120
|
-
import urllib.request
|
|
121
|
-
|
|
122
|
-
with open(path, "rb") as f:
|
|
123
|
-
body = f.read()
|
|
124
|
-
req = urllib.request.Request(url, data=body, method="PUT")
|
|
125
|
-
for k, v in (headers or {}).items():
|
|
126
|
-
req.add_header(k, v)
|
|
127
|
-
with urllib.request.urlopen(req, timeout=300) as r: # noqa: S310 - caller-minted
|
|
128
|
-
return {"status": r.status, "bytes": len(body)}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def save_checkpoint(path):
|
|
132
|
-
"""Whole-scene snapshot. Measured elsewhere at tens to hundreds of ms, and
|
|
133
|
-
it hides inside the agent's think time. What makes a pod disposable."""
|
|
134
|
-
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
|
|
135
|
-
bpy.ops.wm.save_as_mainfile(filepath=path, copy=True, compress=True)
|
|
136
|
-
return os.path.getsize(path)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def load_checkpoint(path):
|
|
140
|
-
"""Restore a snapshot. NOTE: this invalidates every live bpy reference, so
|
|
141
|
-
the exec namespace is reset alongside it -- a held Python handle raises
|
|
142
|
-
`StructRNA has been removed` long after the restore, somewhere unrelated."""
|
|
143
|
-
bpy.ops.wm.open_mainfile(filepath=path)
|
|
144
|
-
reset_namespace()
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def _host_allowed(url):
|
|
148
|
-
"""Empty allowlist = permissive, which is correct for the admin-only spike
|
|
149
|
-
and wrong for the hosted lane. GENEX_BLENDER_IMPORT_HOSTS narrows it without
|
|
150
|
-
a code change when this reaches a session."""
|
|
151
|
-
allow = [h.strip() for h in os.environ.get("GENEX_BLENDER_IMPORT_HOSTS", "").split(",") if h.strip()]
|
|
152
|
-
if not allow:
|
|
153
|
-
return True
|
|
154
|
-
host = (urllib.parse.urlparse(url).hostname or "").lower()
|
|
155
|
-
return any(host == a.lower() or host.endswith("." + a.lower().lstrip(".")) for a in allow)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
def import_glb(url):
|
|
159
|
-
if not _host_allowed(url):
|
|
160
|
-
raise ValueError(f"import host not allowed: {urllib.parse.urlparse(url).hostname}")
|
|
161
|
-
before = {o.name for o in bpy.context.scene.objects}
|
|
162
|
-
fd, tmp = tempfile.mkstemp(suffix=".glb", prefix="genex-import-")
|
|
163
|
-
os.close(fd)
|
|
164
|
-
try:
|
|
165
|
-
with urllib.request.urlopen(url, timeout=60) as r: # noqa: S310 - allowlisted above
|
|
166
|
-
with open(tmp, "wb") as f:
|
|
167
|
-
f.write(r.read())
|
|
168
|
-
bpy.ops.import_scene.gltf(filepath=tmp)
|
|
169
|
-
finally:
|
|
170
|
-
os.unlink(tmp)
|
|
171
|
-
added = [o.name for o in bpy.context.scene.objects if o.name not in before]
|
|
172
|
-
return {"imported": added, "count": len(added)}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
def scene_digest():
|
|
176
|
-
"""A fingerprint of everything a render depends on, for the sheet-repeat guard.
|
|
177
|
-
|
|
178
|
-
Built from the data blocks directly, not from scene_info(): that summary
|
|
179
|
-
carries location, dimensions and material NAMES, and nothing else — so a
|
|
180
|
-
rotation, a scale, a modifier, a moved vertex, a dimmed light or a recoloured
|
|
181
|
-
material all hashed identically and the model was told the picture was
|
|
182
|
-
unchanged (review finding). Vertex coordinates go through numpy, so a 600k
|
|
183
|
-
triangle mesh costs one foreach_get and one hash rather than a Python loop.
|
|
184
|
-
"""
|
|
185
|
-
import hashlib
|
|
186
|
-
h = hashlib.sha256()
|
|
187
|
-
scene = bpy.context.scene
|
|
188
|
-
for obj in scene.objects:
|
|
189
|
-
# The RNA transform, NOT matrix_world: a script that sets rotation_euler
|
|
190
|
-
# leaves matrix_world stale until the depsgraph evaluates, which in
|
|
191
|
-
# background mode is not before this runs. Measured live: a rotation
|
|
192
|
-
# hashed identical through matrix_world and the guard said "unchanged".
|
|
193
|
-
parent = obj.parent.name if obj.parent else ""
|
|
194
|
-
h.update(("%s|%s|%s|%s|%s|%s|%d|%s\n" % (
|
|
195
|
-
obj.name, obj.type, parent, tuple(obj.location),
|
|
196
|
-
tuple(obj.rotation_euler), tuple(obj.scale),
|
|
197
|
-
obj.hide_render, [m.type for m in getattr(obj, "modifiers", [])],
|
|
198
|
-
)).encode("utf-8"))
|
|
199
|
-
data = obj.data
|
|
200
|
-
if obj.type == "MESH" and data is not None:
|
|
201
|
-
n = len(data.vertices)
|
|
202
|
-
if n:
|
|
203
|
-
try:
|
|
204
|
-
import numpy as np
|
|
205
|
-
buf = np.empty(n * 3, dtype=np.float32)
|
|
206
|
-
data.vertices.foreach_get("co", buf)
|
|
207
|
-
h.update(buf.tobytes())
|
|
208
|
-
except Exception:
|
|
209
|
-
h.update(str(n).encode())
|
|
210
|
-
for m in data.materials:
|
|
211
|
-
if m is None:
|
|
212
|
-
continue
|
|
213
|
-
h.update(("mat:%s:%s\n" % (m.name, tuple(m.diffuse_color))).encode())
|
|
214
|
-
if m.use_nodes and m.node_tree:
|
|
215
|
-
for node in m.node_tree.nodes:
|
|
216
|
-
if node.type == "BSDF_PRINCIPLED":
|
|
217
|
-
h.update(("bsdf:%s\n" % (tuple(node.inputs["Base Color"].default_value),)).encode())
|
|
218
|
-
elif obj.type == "LIGHT" and data is not None:
|
|
219
|
-
h.update(("light:%s:%s:%s\n" % (data.type, data.energy, tuple(data.color))).encode())
|
|
220
|
-
elif obj.type == "CAMERA" and data is not None:
|
|
221
|
-
h.update(("cam:%s:%s\n" % (data.lens, data.type)).encode())
|
|
222
|
-
w = scene.world
|
|
223
|
-
if w is not None:
|
|
224
|
-
h.update(("world:%s\n" % (tuple(getattr(w, "color", (0, 0, 0))),)).encode())
|
|
225
|
-
return h.hexdigest()
|