LLM-driven scene generation for Genie Sim. An LLM (hosted in Open WebUI)
writes a small Python program in the Scene Language DSL; that program
builds a Shape tree from the shared asset library, and the package compiles
it into a scene.usda + layout graph that the benchmark/sim stack consumes.
Source: source/geniesim_generator/ License: Mozilla Public License Version 2.0 Skills (Claude Code / opencode): skills/ Repo-wide routing: ../AGENTS.md · package table: ../source/AGENTS.md
Maintenance contract — when you add/rename a DSL primitive or
@registerhelper, change theLLM_RESULT.pyimport contract, add an MCP tool/server, change the output directory layout, or touch the Open WebUI config exports, update this file in the same diff. Agents read this as the source of truth.
Not a CLI verb. Unlike
geniesim benchmark, there is nogeniesim generator …subcommand.geniesim_clionly treats this package as a pip-installable peer (bootstrap/deploy geniesim_generator/status, gated behind the[generator]/[full]extras). All runtime entry points are plainpythoninvocations anddocker compose— see §4.
A four-stage pipeline. The LLM produces DSL code; the package executes and compiles it:
Open WebUI agent (geniesimscenegen)
│ calls MCP tools on :8765 to discover assets + interactions
▼
LLM emits Python: from helper import * → @register()… root_scene()
│ "save" action writes it to → generator/LLM_RESULT.py
▼
app.py: import LLM_RESULT.root_scene → run it → Shape tree
│ helper.gen_scene_layout_info → (scene_info dict, networkx DiGraph)
│ utils.usd.gen_scene_usda → scene.usda
▼
benchmark/config/llm_task/<scene_id>/<n>/{scene.usda, scene_info.json, graph.svg, LLM_RESULT.py}
▲
scene_viewer.py file-watches LLM_RESULT.py and live-reloads scene.usda into Isaac Sim
The DSL (scene_language/) is adapted from the public Scene Language
project (Zhang et al., CVPR 2025) — see scene_language/README.md
and scene_language/LICENSE.md.
It has been specialized to drive Genie Sim's ASSETS_INDEX (from the
geniesim_assets package).
| Artifact | Location |
|---|---|
Generator entry (main) |
src/geniesim_generator/app.py |
DSL execution surface (from helper import *) |
src/geniesim_generator/helper.py |
| LLM program slot / template | src/geniesim_generator/LLM_RESULT.py |
| Isaac Sim live preview | src/geniesim_generator/scene_viewer.py |
| Scene-Language DSL | src/geniesim_generator/scene_language/ |
USD serializer (gen_scene_usda) |
src/geniesim_generator/utils/ |
| MCP servers + RAG | src/geniesim_generator/server/ |
| Open WebUI config exports | src/geniesim_generator/config/ |
| Docker stack (MCP + Open WebUI) | src/geniesim_generator/compose.yaml |
| Deploy / status integration | ../geniesim_cli/src/geniesim_cli/commands/ (deploy.py, status.py, bootstrap.py) |
- Add/change a generator output (USD, graph, json) →
app.py:main+helper.gen_scene_layout_info+utils/usd.py - Add/change a DSL surface the LLM can call →
helper.py(re-exports +usd(),get_*_info) andscene_language/ - DSL registration / lookup (
register,library_call, theinfo["stack"]frame) →scene_language/dsl_utils.py - Shape composition (
concat_shapes,transform_shape, bbox math) →scene_language/shape_utils.py+_shape_utils.py - Primitives (
cube/sphere/cylinder) →scene_language/engine_utils.py→_engine_utils_exposed.py(the only supportedENGINE_MODE="exposed") - Spatial helpers (
attach,align_with_*) →scene_language/calc_utils.py; assertions →assert_utils.py - Offline Mitsuba rendering →
scene_language/mi_helper.py+engine/utils/mitsuba_utils.py - Asset search (RAG) →
server/mcp_assets_server.py→server/assets_searcher/(AssetVectorDBtext,AssetVectorDBVLvision-language) - Asset interaction metadata →
server/mcp_assets_info.py(readsASSETS_INDEX.interaction.{active,passive}) - LLM file writes →
server/mcp_file_server.py(MCP tool) andserver/save_to_local.py/config/save_data_gen.py(Open WebUI actions)
# Generate one scene from the current LLM_RESULT.py
# (script-relative imports — run from the package dir, NOT via `python -m`)
cd src/geniesim_generator && PYTHONPATH=../.. python app.py --scene_id <id> [--task_gen]
# Live preview in Isaac Sim (watches LLM_RESULT.py, reloads on save)
python src/geniesim_generator/scene_viewer.py [--auto-play]
# MCP servers (stdio JSON-RPC; wired by server/mcp_config.json)
python src/geniesim_generator/server/mcp_assets_server.py
python src/geniesim_generator/server/mcp_assets_info.py
python src/geniesim_generator/server/mcp_file_server.py
# Full stack: MCP gateway (:8765) + Open WebUI, via Docker — pick ONE embedding profile (see §6)
# Prereq: pip install geniesim_assets on host, then derive GENIESIM_ASSETS_DIR
# from the running Python so compose can mount the host's installed copy.
export GENIESIM_ASSETS_DIR=$(python -c \
"import geniesim_assets, os; print(os.path.dirname(geniesim_assets.__file__))")
docker compose --profile text up --build # Qwen embedding API (no GPU; needs API key)
docker compose --profile vl up --build # Qwen3-VL embedding (local GPU + NVIDIA Container Toolkit)app.py uses script-relative imports (from helper import *,
from LLM_RESULT import root_scene), so it must run with the package dir on
the path / as cwd. Outputs land under
benchmark/config/llm_task/<scene_id>/<n>/. See
server_readme.txt for text-vs-VL
deployment details.
Install with pip install -e source/geniesim_generator[<extra>].
| Extra | Pulls in | Required by |
|---|---|---|
| (core) | mitsuba, networkx, numpy, pillow, scipy, usd-core, geniesim_assets, … | app.py, helper.py, scene_viewer.py, scene_language/*, utils/usd.py |
mcp |
fastmcp, pydantic, aiofiles | all server/mcp_*.py, server/save_to_local.py, config/save_data_gen.py |
rag |
chromadb, openai, dashscope | server/assets_searcher/assets_searcher.py, embeddings/text_embedding.py (text mode, EMBEDDING_MODE=text) |
vl |
torch, typing-extensions | server/assets_searcher/assets_searcher_vl.py, embeddings/vl_embedding.py (default EMBEDDING_MODE=vl) |
full |
mcp + rag + vl | the entire server/ stack |
Isaac Sim (isaacsim.SimulationApp, used by scene_viewer.py) is not a
pyproject dependency — it is provided by the external sim runtime.
Asset RAG search (server/mcp_assets_server.py) needs an embedding backend.
The two docker compose profiles are mutually exclusive — both publish the
MCP gateway on :8765, so run only one at a time. Choose by what hardware /
credentials you have. Authoritative source: server_readme.txt.
Both profiles need geniesim_assets pip install-ed on the host. Before
bringing the stack up, derive GENIESIM_ASSETS_DIR from the running Python so
compose mounts the host's installed copy into /opt/geniesim_assets (read-only;
the multi-GB asset library is not baked into the image). entrypoint.sh
adds /opt to PYTHONPATH to make the mount importable. compose.yaml errors
out fast if the variable is unset. No host paths are baked into the image.
export GENIESIM_ASSETS_DIR=$(python -c \
"import geniesim_assets, os; print(os.path.dirname(geniesim_assets.__file__))")Calls a remote embedding API (Dashscope's OpenAI-compatible endpoint). Use this when you don't have a GPU but can provide an API key.
# 1. Put your key + endpoint in the text config
# server/mcp_text_embedding/text_embedding_config.json
# { "api_key": "<YOUR_DASHSCOPE_KEY>",
# "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
# "dashscope_mode": true, "dimension": 2048, "model": "text-embedding-v4" }
# 2. From the generator package dir (GENIESIM_ASSETS_DIR already exported, see above):
docker compose --profile text up --build- Pros: no GPU, fast to stand up. Cons: needs network + a paid API key; text-only (no image/video understanding of assets).
- Backend class:
AssetVectorDB; embedder:DashscopeTextEmbeddings/TextEmbeddings.
Runs Qwen3-VL-Embedding locally on the GPU — no external API, and it embeds asset images as well as text (better retrieval quality). Requires an NVIDIA GPU and the NVIDIA Container Toolkit.
# 1. Download weights + upstream helper scripts (ModelScope by default; --huggingface to switch)
cd server/assets_searcher
bash download_model.sh # 2B by default; also accepts 8B | ALL
# → fetches Qwen3-VL-Embedding-2B + Qwen3-VL-Reranker-2B into ./models/
# 2. Tune services/mcp-server_vl/environment in compose.yaml for your card
# BATCH_SIZE (default 10 ≈ RTX 4090; larger = more VRAM, smaller = slower indexing)
# USE_RERANKER (True/False; reranker improves quality but costs latency + VRAM)
# 3. From the generator package dir (GENIESIM_ASSETS_DIR already exported, see above):
docker compose --profile vl up --build- Pros: fully local, image-aware, optional reranker. Cons: needs a CUDA GPU + container toolkit, VRAM for the model, weight download.
- Backend class:
AssetVectorDBVL; embedder:QwenVLEmbeddings(+ optional Qwen3-VL-Reranker). The image (nvcr.io/nvidia/pytorch) already bundles torch/transformers/qwen-vl-utils — only the weights are downloaded.
| Var | Profile | Effect |
|---|---|---|
EMBEDDING_MODE |
both | vl → AssetVectorDBVL; text → AssetVectorDB (set per profile, don't override) |
VL_EMBEDDING_MODEL |
vl | VL embedder weights, e.g. Qwen3-VL-Embedding-2B |
USE_RERANKER |
vl | Enable the Qwen3-VL reranker on top of VL search (True/False) |
PERMANENT_MODEL_IN_GPU |
vl | Keep the VL model resident (else evicted after ~5s idle) |
BATCH_SIZE |
vl | Index-build batch size; trade VRAM vs. speed |
ChromaDB cache is bind-mounted under server_chromadb* → /tmp/chromadb_cache*
(see compose.yaml). The open-webui service runs on host networking
regardless of profile and reads the tool servers at http://localhost:8765.
vl cold-start caveats (details + fixes in the deploy-generator skill):
the first vl launch builds the full vector index on the GPU (can take tens of
minutes for the full library), during which mcpo times out connecting to
assets-agent and /assets-agent/* returns 404 — expected; restart once
Sync completed is logged and the cached index makes startup instant. The 2B model peaks
~16 GB VRAM; on a shared card lower BATCH_SIZE and set
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True to avoid CUDA OOM. The
weights models/ dir must sit inside server/assets_searcher/ (covered by the
mount) — an out-of-tree symlink dangles in-container → ModuleNotFoundError.
These are exported Open WebUI artifacts, not auto-loaded by the package — import them into your Open WebUI instance.
| File | What it is |
|---|---|
config/geniesimscenegen.json |
The scene-generator model (emits DSL programs) |
config/geniesimassets.json |
The asset-search model (uses the MCP tools) |
config/openwebui.json |
Workspace config: Dashscope endpoint + the three tool servers on :8765 |
config/function-save_code_to_file.json |
Open WebUI function export of server/save_to_local.py |
config/save_data_gen.py |
Open WebUI action: parse root_scene() from chat → write benchmark data_gen.py |
app.pyalways importsroot_scenefromLLM_RESULT.py. Keep that name and thefrom helper import *contract stable — the LLM and the templates depend on it.- The DSL stack frame is the bridge to layout: every
@register-decorated function pushes(func_name, uuid)onto each returned shape'sinfo["stack"], andhelper.gen_scene_layout_infowalks exactly that. Do not change the frame shape without updating the walker. helper.usd(oid, keywords)is the canonical way DSL code instantiates anASSETS_INDEXentry. New asset-driven primitives go through it, carrying the asset id + keywords ininfo.- Only
ENGINE_MODE="exposed"is implemented (engine_utils.pyraisesNotImplementedErrorotherwise). Don't reference other engine modes. scene_language.primitive_callis an unimplementedHoleuntilimport geniesim_generator.scene_language.mi_helperruns (app.pyline 18 does this). Without that import it degrades to a placeholder and dropsinfo["stack"]→KeyError: 'stack'. Always compile DSL programs throughapp.py; if you execute one elsewhere, importmi_helperfirst.- Asset metadata is owned by the
geniesim_assetspackage (ASSETS_INDEX), not by this package — read it, don't fork it.
The skills/ directory holds opencode/Claude-Code-style playbooks
for the common generator workflows. They're tool-agnostic markdown with YAML
frontmatter; symlink or copy them into ~/.claude/skills/ (or
.opencode/skills/) to enable auto-trigger.
| Skill | Trigger |
|---|---|
| deploy-generator | "部署 generator", "deploy the scene generator", "启动资产检索服务", "start the MCP assets server" |
| search-assets | "搜索资产", "find an asset", "search the asset library", "look up asset_id X" |
| generate-scene | "生成一个场景", "按需求生成场景", "绕过 webui 生成场景", "generate a scene", "make a scene with <objects>" |
The three chain naturally: deploy-generator (bring up :8765 + Open WebUI)
→ search-assets (discover real asset ids) → generate-scene (write the
DSL program and compile scene.usda).
- Don't add a
geniesim generatorCLI verb to fake a runtime command — the package's contract is "pip peer + python entry points + docker." If a verb is genuinely wanted, add it deliberately ingeniesim_cliand document it here. - Don't hardcode remote hosts, API keys, or ports inside DSL code or configs — endpoints/keys come from env vars and the Open WebUI workspace config.
- Don't pin asset ids/bboxes by hand in DSL code — resolve them through
helper.usd()/get_*_info()againstASSETS_INDEX. - Don't put MCP/RAG/VL-only imports (
fastmcp,chromadb,torch, …) into the core import path (app.py/helper.py) — keep them insideserver/so the core generator installs without the heavy extras.