File size: 13,624 Bytes
77b3711
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7bb2fda
 
 
 
 
77b3711
 
 
 
 
 
 
 
 
 
7bb2fda
77b3711
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98ca16a
 
 
 
 
 
 
 
 
 
 
77b3711
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5b66adc
 
 
 
 
 
 
 
 
 
6287002
 
 
 
 
 
 
 
 
 
 
5b66adc
 
 
 
 
 
 
 
77b3711
 
 
 
 
6287002
f9a8763
6287002
af80e6b
2fe7d5d
7bb2fda
 
 
77b3711
 
7bb2fda
65f605e
 
 
 
e0f9b43
 
7bb2fda
af80e6b
 
 
 
64b0f23
 
 
 
af80e6b
92d401f
 
 
 
 
 
 
 
 
363fb28
 
 
e1d8e60
2fe7d5d
 
 
e1d8e60
 
 
 
 
92d401f
7bb2fda
 
 
 
 
 
f9a8763
e0f9b43
 
f9a8763
 
 
 
 
 
 
 
77b3711
7bb2fda
 
77b3711
 
 
92d401f
 
447d1d2
 
7bb2fda
 
 
77b3711
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# VisualLearningIdea: v1 spec

Supersedes `visuallearning-handover.html`, which drifted over several HuggingChat
sessions. `cat-sun-scene.html` stays as the UX reference for the serial reading.

## Idea

3D is a language. The Space **translates English into 3D**, the way you would
translate English into Japanese. A picture is easier to take in than text, because
the reader doesn't have to guess the exact picture. Once 3D exists it can be
materialized (papercraft, origami, …). The **simplification ladder** shows how far
a mesh can be reduced and still mean its word, and that is a main output.

One Gradio Space serves people (UI) and models (`demo.launch(mcp_server=True)`).
HuggingChat models can't run tools, which is why this Space exists.

**The Space visualises; discussion happens in the chat** (e.g. HuggingChat over MCP).
The calling model already sees the picture, so the Space runs no vision model of
its own and stores no token. Because HTML is the final destination, every picture
comes back with a **mini HTML page** the model can read, edit and build on.

## Translation terms

| Translation | Here |
|---|---|
| Source β†’ target language | English β†’ 3D |
| Dictionary | Ready assets: SimReady β†’ Objaverse |
| Grammar words | `the / a / and`: no geometry; a determiner opens a "which?" question |
| Verbs | Relations between meshes |
| Word alignment | `tokens[].commits`: reading order differs from build order |
| Forced choices | `unresolved`: which asset, how big |
| Back-translation | The chat model reading the picture: done in the discussion, not by the Space |

## Dictionary

- Tiers, each taken **on rejection**: `simready β†’ objaverse β†’ primitive β†’ generation`.
- **Available** = a SimReady or Objaverse asset exists (no construction). No asset
  means not available, with no exceptions. The UI offers only available words, so
  the user knows before submitting.
- A missing word can be used only by **explicitly choosing a primitive** for it
  (`box, sphere, cylinder, cone, plane`). In the UI the user picks one. Over MCP,
  `check_words` returns `missing` and the user's model (e.g. in HuggingChat) decides:
  rephrase, or pass `primitives_json` such as `{"sun": "sphere"}`.
- Assets that **can't be used** are skipped and recorded in `decision.skipped`:
  no license, NC (non-commercial) or ND (no derivatives; simplification is a
  derivative), or no geometry.
- SimReady: package list from `NVIDIA-Omniverse/simready-central/submissions/*`.
  Only geometry USD + thumbnail are downloaded, because textures are ~98% of each
  ~1 GB package. Assets are metric, so height comes from the asset.
- Objaverse: LVIS categories (`lvis-annotations.json.gz`) are the word index; the
  license comes from `metadata/<shard>.json.gz`. No real scale, so height is a
  nominal default (an `unresolved` decision the user can set).
- Light comes from the verb, not from a special word: whatever "shone" becomes the
  light source and sits in the sky along the light direction.
- When nothing shines, the picture still needs light: seeing needs light as sitting
  needs a ground. The scene gets soft daylight from no object (`sun.daylight: true`,
  seeded direction, shadows on) and `unresolved` asks "what light?" (default:
  daylight; rephrase with "<noun> shone/glowed" to make an object the light).
- SimReady geometry comes without textures, so the colour is the mean of the asset's
  256 px thumbnail (backdrop and transparent pixels left out), stored as `item.color`.
  `item.thumbnail` is a public dataset URL, so the chat can show and compare assets.
  Uncoloured meshes render neutral light grey.
- A "which <word>?" question carries `on_reject`: what rejecting the tier would offer
  (e.g. `{"tier": "objaverse", "candidates": 101, "offered": 9}`), since those
  alternatives are invisible until the tier is rejected.
- The ground implied by "sat" (with no stated object) is a support plane created by
  the relation. It is not a typed word, so it needs no asset. To exist, a thing needs
  a reality to exist in: reading skips this, but visualising exposes it. So the
  implied ground is also surfaced in `unresolved` ("cat sat on what?", default:
  ground plane), for the discussion to accept or rephrase.

## Serial side vs configuration side

- **Serial (ledger)**: tokens in reading order, items each word commits, relations,
  unresolved questions. **Freezing lives here.** Frozen items can't change; the
  others can be swapped, rejected, resized or rerolled.
- **Configuration**: once the serial side is resolved, any placement satisfying the
  relations is valid. `seed` picks one (positions, yaw, sun direction).

### Ledger shape

```jsonc
{
  "sentence": "the cat sat and the sun shone",
  "tokens": [{"index": 2, "text": "sat", "kind": "verb", "commits": ["ground", "r2"], "opens": []}],
  "items": [
    {"id": "n1", "kind": "object", "word": "cat", "token": 1, "frozen": false,
     "decision": {"tier": "objaverse", "chosen": {…}, "alternatives": […],
                  "rejected_tiers": [], "skipped": [], "label": "cat"},
     "height_m": {"value": 0.3, "source": "default"}, "license": "by"},
    {"id": "ground", "kind": "environment", "role": "plane", "implied_by": 2},
    {"id": "r2", "kind": "relation", "relation": "on", "subject": "n1", "object": "ground"},
    {"id": "n5", "kind": "object", "word": "sun", "emits_light": true,
     "decision": {"tier": "primitive", "chosen": {"source": "primitive", "shape": "sphere"}, …}},
    {"id": "r6", "kind": "relation", "relation": "lights", "subject": "n5", "object": "scene"}
  ],
  "unresolved": [{"id": "q0", "about": "n1", "question": "which cat?", "default_chosen": "cat", "alternatives": […]}],
  "configuration": {"seed": 0, "placements": {"n1": {"position": [0, 0, 0], "yaw_deg": 12}}, "sun": {…}}
}
```

Relations: `on, under, beside, near, behind, in_front_of, above, faces, lights`.
Verbs: `sat/stood/lay/rested` β†’ `on` (ground implied), `shone/glowed` β†’ `lights`
(shadows only turn on with this coupling), `faced` β†’ `faces`.

## Paragraphs: sentences + bindings

`extend_scene` adds the next sentence to an existing ledger (token and item ids carry
on, `sentences` lists them). Within a sentence or across sentences:

- **"the <noun>"** when that noun is already in the ledger refers to it (the most
  recent one) instead of adding an item. Surfaced as an implied question: "which cat?
  the one already mentioned" (default: the same cat), alternatives include "say
  'another cat' for a new one".
- **"a/an/another <noun>"** always adds a new item.
- **Pronouns** (it, he, she, they, him, her, them) refer back by a rule, `pronouns`
  on `translate_to_scene`/`extend_scene`, kept in the ledger:
  - `"recent"` (default): the most recent object.
  - `"subject"`: the subject of the most recent clause, else the most recent object.
    Closer to how readers take "it": in "the cat sat on the table. the apple lay
    beside it", `recent` gives the table, `subject` the cat.

  Either way the clause's own subject is left out when the pronoun is its object ("the
  apple lay beside it" can't mean the apple), and the choice is surfaced as "'it' = the
  cat?" with the other objects as alternatives, most recent first. A pronoun with
  nothing before it is an error.
- Each reference question carries `binds: {token, to}`; relations record which token
  named each end (`tokens: {subject, object}`), so a reference can be rebound.
- **`bind`** (a `refine_scene` change): `{"id": "q10", "bind": "n1"}` rebinds a
  reference question; `{"id": "n12", "bind": "n1"}` merges a duplicate object into
  another (relations move, its "which?" question goes, a light carries over). Both
  refuse to relate an object to itself and add a note to `resolutions`.
- "what light?" is asked once per ledger and dropped when a later sentence lights the scene.

## MCP tools

| Tool | In β†’ out |
|---|---|
| `check_words` | sentence, primitives β†’ per-word status, missing words, completions |
| `translate_to_scene` | sentence, primitives, seed, html, pronouns β†’ ledger, PNG, GLB, mini HTML file, mini HTML text |
| `refine_scene` | ledger, changes, seed, html β†’ ledger, PNG, GLB, mini HTML file, mini HTML text |
| `extend_scene` | ledger, sentence, primitives, seed, html, pronouns β†’ ledger, PNG, GLB, mini HTML file, mini HTML text |
| `simplify_item` | ledger, item id, html, rings, samples β†’ report, rung PNGs, rung GLBs, mini HTML file, mini HTML text |
| `papercraft_rings` | ledger, item id, rings, samples, height_cm β†’ pieces report, PNG per A4 page (to look at), SVG per A4 page (mm, to print) |

`check_words` also returns `maps_to` per noun (the asset it would get), so a word
that exists but maps to the wrong thing shows up before submitting.

Ladder rungs: original β†’ 20k / 5k / 1k / 250 / 60 faces β†’ convex hull β†’ oriented
box. Where the object stops reading as its word is judged in the discussion.
Decimation welds split vertices (UV and normal seams) between passes. When quadric
decimation still stalls (CAD meshes with sliver triangles, e.g. a SimReady lamp stuck
at ~21k faces), vertex clustering takes over: cruder, but it always reduces and keeps
thin parts. Rungs are
labelled with the faces they actually have, and the ladder stops descending when
a rung doesn't reduce, so a stalled mesh never shows as several identical rungs.

**Rings rung (second path, for papercraft).** `rings` horizontal sections, each
outline sampled at `samples` evenly spaced angles from the section's centre (the
farthest crossing per ray), joined into bands with flat caps. Every band is one
paper strip and every vertex is (ring, angle); the report lists each ring's height,
centre and radii, so it can be checked by hand. One stack per object: each ray takes
the farthest crossing, so disjoint outlines in one section merge (two legs become one
band bridging the gap), branching or overhanging parts (a lamp's arm and shade) merge
into one envelope, and heights with no section are skipped.

### Papercraft (rings)

`papercraft_rings` turns the rings rung into a printable net (SVG, millimetres, real size
or scaled to `height_cm`). Each band between two rings unrolls exactly into one strip:
its triangles share edges in sequence, so laying them flat keeps every length. A band
that would curl past ~120Β° on paper overlaps itself, so it is split into parts (2a, 2b).
Every joint has glue tabs on one side only, labelled with the piece they meet (band top
edge β†’ next band or top cap, seam end β†’ own start or next part, bottom cap rim β†’ band 1).
Caps under 4 mm are dropped and the band closes to a point; tabs under 2 mm are dropped.
Cut solid lines, fold dashed lines. The report lists, per piece, `glues_to` (the tabs it
carries) and `receives_from` (pieces whose tabs land on it), so a tabless piece such as
the top cap still shows its joint.

Output is A4 pages (190 Γ— 277 mm inside 10 mm margins), each as a 150 dpi PNG (chats,
`<img>` and fetchers show PNGs; SVG files come back without a MIME type) and as an SVG
in millimetres for printing. Each piece is turned to its
narrowest orientation and shelf-packed; strips longer than a page are split like curled
ones. With `height_cm` blank the model is the tallest whose every piece fits a page,
which is set by band width and cap size, not strip length. The report gives
`real_height_cm`, `max_height_cm_for_a4` and `fits_a4`; a piece too big for A4 at a
requested height gets a page of its own size.

### Mini HTML

A self-contained page: Three.js (`three@0.160.0` from unpkg) plus one inline `D`
data object holding the sentence, the reading (word β†’ commits), relations,
unresolved questions, sun, ground, and each object's position, yaw and colour.
Each object also has either inline low-poly geometry (≀400 faces in a scene,
rungs ≀1000 faces in a ladder) or a primitive spec. Vertices are rounded to cm.
Each inline mesh states `faces`, `budget` and `floor`: `floor: true` means the mesh
wouldn't decimate down to the budget, so the page carries more than asked.
The geometry is inline, so a saved page works after Gradio's temporary file URLs expire.

**Geometry is for the handoff, not the loop.** Even at 400 faces a page costs
thousands of tokens, which a multi-turn chat can't afford on every refine. So
`html` defaults to `"url"`: the page comes back as a file URL and the text field is
empty. Models iterate on the ledger and the picture, and pass `html="inline"` only
when they build on the page (`"skip"` omits it). An expired URL costs nothing: the
ledger fully determines the scene, and the next refine re-creates the page.

**No secrets.** The Space is public and stores no token; the datasets are public
and download without one. Optional env: `VLI_CACHE`.

## Known gaps / open questions

- **Materialization**: only the rings path exists (`papercraft_rings`). Nets from the
  decimated rungs (general mesh unfolding) are not built yet.
- **Generation tier** is a stub: rejecting a primitive returns a clear error before
  changing the ledger (use `shape` or `height_m` instead).
- SimReady names β†’ words is a heuristic: the name after its category is indexed
  as a whole phrase plus its final part (`interiorPart_sun_visor_a01` β†’ "sun visor",
  "visor"; never "sun" alone). `maps_to` makes the remaining misses visible.
- Objaverse "front" direction is unknown, so `faces` assumes +Z forward.
- `on` uses the bounding-box top, so objects on irregular tops may float or sink.
- Renders are matplotlib (headless, CPU): approximate shading and a flat shadow on
  the ground plane only.