kwabs22 Claude Opus 5.5 commited on
Commit
447d1d2
·
1 Parent(s): 7bb2fda

Act on the 2026-10-30 QA pass: reroll walks the pool, clearer questions and errors

Browse files

- Reroll takes the next alternative (old choice to the back) instead of a
seed-drawn slot, so repeated rerolls visit the whole pool and the seed
only picks the configuration.
- "which <word>?" options carry a short uid, so Objaverse candidates that
share a category are distinguishable.
- Rejecting a primitive raises a clear error before touching the ledger
(generation is not wired up yet); docs say so.
- Example sentence is now "the cat sat and the lamp glowed", which needs
no primitives.
- decimate hands fast-simplification writable copies and drops the old
positional fallback, which newer trimesh reads as a percentage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Files changed (5) hide show
  1. SPEC.md +2 -1
  2. app.py +9 -6
  3. vli/grammar.py +2 -1
  4. vli/pipeline.py +16 -10
  5. vli/simplify.py +4 -4
SPEC.md CHANGED
@@ -123,7 +123,8 @@ and download without one. Optional env: `VLI_CACHE`.
123
  ## Known gaps / open questions
124
 
125
  - **Materialization** (papercraft from a low rung) is not built yet.
126
- - **Generation tier** is a stub: rejecting a primitive errors out.
 
127
  - SimReady names → words is a heuristic: the name after its category is indexed
128
  as a whole phrase plus its final part (`interiorPart_sun_visor_a01` → "sun visor",
129
  "visor"; never "sun" alone). `maps_to` makes the remaining misses visible.
 
123
  ## Known gaps / open questions
124
 
125
  - **Materialization** (papercraft from a low rung) is not built yet.
126
+ - **Generation tier** is a stub: rejecting a primitive returns a clear error before
127
+ changing the ledger (use `shape` or `height_m` instead).
128
  - SimReady names → words is a heuristic: the name after its category is indexed
129
  as a whole phrase plus its final part (`interiorPart_sun_visor_a01` → "sun visor",
130
  "visor"; never "sun" alone). `maps_to` makes the remaining misses visible.
app.py CHANGED
@@ -92,7 +92,7 @@ def translate_to_scene(sentence: str, primitives_json: str = "{}", seed: str = "
92
  with no asset must be mapped to a primitive in primitives_json (see check_words).
93
 
94
  Args:
95
- sentence: A sentence using available words, e.g. "the cat sat and the sun shone".
96
  primitives_json: JSON object mapping missing words to shapes, e.g. {"sun": "sphere"}.
97
  seed: Picks one of the configurations that satisfy the ledger (positions, facing, sun).
98
 
@@ -112,24 +112,27 @@ def refine_scene(ledger_json: str, changes_json: str = "[]", seed: str = "0") ->
112
  {"id": "n1", "freeze": true} lock an item (false unlocks)
113
  {"id": "n1", "use": 2} swap in alternatives[2]; the old choice takes slot 2,
114
  so using 2 again swaps back (other slots don't move)
115
- {"id": "n1", "reject": true} reject the tier: simready -> objaverse -> primitive -> generation
 
116
  {"id": "n1", "height_m": 0.4} set the object's height in metres
117
  {"id": "n1", "shape": "sphere"} use a primitive (box, sphere, cylinder, cone, plane)
118
- {"reroll": true} every unfrozen object takes a random alternative
 
119
  Frozen items cannot be changed. The configuration (placement, sun angle) is always
120
  re-chosen from `seed`: change the seed for another valid configuration.
121
 
122
  Args:
123
  ledger_json: Ledger JSON from translate_to_scene or a previous refine_scene.
124
  changes_json: JSON list of changes, as above.
125
- seed: Picks the configuration, and the alternatives for a reroll.
 
126
 
127
  Returns:
128
  (ledger JSON string, rendered PNG, GLB scene file, mini HTML page).
129
  """
130
  try:
131
  ledger = _json(ledger_json, "ledger_json")
132
- pipeline.apply_changes(ledger, _json(changes_json, "changes_json", []), _seed(seed))
133
  return pipeline.outputs(ledger, _seed(seed))
134
  except Exception as e:
135
  _fail(e)
@@ -239,7 +242,7 @@ with gr.Blocks(title="VisualLearningIdea") as demo:
239
  "verbs and prepositions are relations between meshes.")
240
  gr.Markdown(DATASETS_MD)
241
  with gr.Row():
242
- sentence = gr.Textbox(label="Sentence", placeholder="the cat sat and the sun shone", scale=4)
243
  seed = gr.Textbox(label="Configuration seed", value="0", scale=1)
244
  status = gr.Markdown()
245
  suggest = gr.Radio(label="Suggestions", choices=[], interactive=True)
 
92
  with no asset must be mapped to a primitive in primitives_json (see check_words).
93
 
94
  Args:
95
+ sentence: A sentence using available words, e.g. "the cat sat and the lamp glowed".
96
  primitives_json: JSON object mapping missing words to shapes, e.g. {"sun": "sphere"}.
97
  seed: Picks one of the configurations that satisfy the ledger (positions, facing, sun).
98
 
 
112
  {"id": "n1", "freeze": true} lock an item (false unlocks)
113
  {"id": "n1", "use": 2} swap in alternatives[2]; the old choice takes slot 2,
114
  so using 2 again swaps back (other slots don't move)
115
+ {"id": "n1", "reject": true} reject the tier: simready -> objaverse -> primitive
116
+ (generation is not wired up: a primitive can't be rejected)
117
  {"id": "n1", "height_m": 0.4} set the object's height in metres
118
  {"id": "n1", "shape": "sphere"} use a primitive (box, sphere, cylinder, cone, plane)
119
+ {"reroll": true} every unfrozen object takes its next alternative;
120
+ repeated rerolls visit the whole pool, whatever the seed
121
  Frozen items cannot be changed. The configuration (placement, sun angle) is always
122
  re-chosen from `seed`: change the seed for another valid configuration.
123
 
124
  Args:
125
  ledger_json: Ledger JSON from translate_to_scene or a previous refine_scene.
126
  changes_json: JSON list of changes, as above.
127
+ seed: Picks the configuration (placement, sun). Placement scales with each
128
+ asset's size, so swapping an asset shifts offsets even at the same seed.
129
 
130
  Returns:
131
  (ledger JSON string, rendered PNG, GLB scene file, mini HTML page).
132
  """
133
  try:
134
  ledger = _json(ledger_json, "ledger_json")
135
+ pipeline.apply_changes(ledger, _json(changes_json, "changes_json", []))
136
  return pipeline.outputs(ledger, _seed(seed))
137
  except Exception as e:
138
  _fail(e)
 
242
  "verbs and prepositions are relations between meshes.")
243
  gr.Markdown(DATASETS_MD)
244
  with gr.Row():
245
+ sentence = gr.Textbox(label="Sentence", placeholder="the cat sat and the lamp glowed", scale=4)
246
  seed = gr.Textbox(label="Configuration seed", value="0", scale=1)
247
  status = gr.Markdown()
248
  suggest = gr.Radio(label="Suggestions", choices=[], interactive=True)
vli/grammar.py CHANGED
@@ -125,7 +125,8 @@ def next_tier(decision, word):
125
  return {"tier": tier, "chosen": cands[0], "alternatives": cands[1:9]}
126
  if tier == "primitive":
127
  return {"tier": tier, "chosen": primitive_candidate(), "alternatives": []}
128
- return {"tier": tier, "chosen": {"source": "generation", "prompt": word}, "alternatives": []}
 
129
  raise ValueError(f"'{word}' has no tier left after {decision['tier']}")
130
 
131
 
 
125
  return {"tier": tier, "chosen": cands[0], "alternatives": cands[1:9]}
126
  if tier == "primitive":
127
  return {"tier": tier, "chosen": primitive_candidate(), "alternatives": []}
128
+ raise ValueError(f"'{word}' has no tier left after {decision['tier']}: generation is not "
129
+ f"wired up yet. Use shape or height_m instead, or rephrase.")
130
  raise ValueError(f"'{word}' has no tier left after {decision['tier']}")
131
 
132
 
vli/pipeline.py CHANGED
@@ -7,8 +7,6 @@ import json
7
  import os
8
  import tempfile
9
 
10
- import numpy as np
11
-
12
  from . import assets, html, scene
13
  from .grammar import build_ledger, candidate_label, next_tier, parse_primitives, primitive_candidate
14
 
@@ -17,6 +15,12 @@ def _workdir():
17
  return tempfile.mkdtemp(prefix="vli_")
18
 
19
 
 
 
 
 
 
 
20
  def settle_all(ledger):
21
  """Load every object's mesh; unusable candidates are skipped and recorded."""
22
  meshes = {}
@@ -28,8 +32,8 @@ def settle_all(ledger):
28
  for q in ledger["unresolved"]:
29
  about = next((i for i in ledger["items"] if i["id"] == q.get("about")), None)
30
  if about and about["kind"] == "object":
31
- q["default_chosen"] = about["decision"]["label"]
32
- q["alternatives"] = [candidate_label(c) for c in about["decision"]["alternatives"]]
33
  return meshes
34
 
35
 
@@ -47,24 +51,25 @@ def translate(sentence, primitives=None, seed=0):
47
  return outputs(build_ledger(sentence, primitives), seed)
48
 
49
 
50
- def apply_changes(ledger, changes, seed=0):
51
  """changes: list of
52
  {"id": ..., "freeze": true|false}
53
  {"id": ..., "use": <index into alternatives>}
54
  {"id": ..., "reject": true} -> next tier
55
  {"id": ..., "height_m": 0.4}
56
  {"id": ..., "shape": "sphere"} -> primitive tier with that shape
57
- {"reroll": true} -> every unfrozen object takes a random alternative
 
 
58
  """
59
  by_id = {i["id"]: i for i in ledger["items"]}
60
- rng = np.random.default_rng(seed)
61
  for ch in changes:
62
  if ch.get("reroll"):
63
  for it in ledger["items"]:
64
  dec = it.get("decision")
65
  if it["kind"] == "object" and not it["frozen"] and dec["alternatives"]:
66
- k = int(rng.integers(len(dec["alternatives"])))
67
- dec["alternatives"][k], dec["chosen"] = dec["chosen"], dec["alternatives"][k]
68
  continue
69
  it = by_id.get(ch.get("id"))
70
  if it is None:
@@ -81,8 +86,9 @@ def apply_changes(ledger, changes, seed=0):
81
  k = int(ch["use"])
82
  dec["alternatives"][k], dec["chosen"] = dec["chosen"], dec["alternatives"][k]
83
  if ch.get("reject"):
 
84
  dec["rejected_tiers"].append(dec["tier"])
85
- dec.update(next_tier(dec, it["word"]))
86
  if it["height_m"]["source"] != "user":
87
  it["height_m"] = {"value": None, "source": "pending"}
88
  if "shape" in ch:
 
7
  import os
8
  import tempfile
9
 
 
 
10
  from . import assets, html, scene
11
  from .grammar import build_ledger, candidate_label, next_tier, parse_primitives, primitive_candidate
12
 
 
15
  return tempfile.mkdtemp(prefix="vli_")
16
 
17
 
18
+ def _option_label(cand):
19
+ """Objaverse candidates share a category name, so the uid tells them apart."""
20
+ label = candidate_label(cand)
21
+ return f"{label} [{cand['uid'][:6]}]" if cand.get("uid") else label
22
+
23
+
24
  def settle_all(ledger):
25
  """Load every object's mesh; unusable candidates are skipped and recorded."""
26
  meshes = {}
 
32
  for q in ledger["unresolved"]:
33
  about = next((i for i in ledger["items"] if i["id"] == q.get("about")), None)
34
  if about and about["kind"] == "object":
35
+ q["default_chosen"] = _option_label(about["decision"]["chosen"])
36
+ q["alternatives"] = [_option_label(c) for c in about["decision"]["alternatives"]]
37
  return meshes
38
 
39
 
 
51
  return outputs(build_ledger(sentence, primitives), seed)
52
 
53
 
54
+ def apply_changes(ledger, changes):
55
  """changes: list of
56
  {"id": ..., "freeze": true|false}
57
  {"id": ..., "use": <index into alternatives>}
58
  {"id": ..., "reject": true} -> next tier
59
  {"id": ..., "height_m": 0.4}
60
  {"id": ..., "shape": "sphere"} -> primitive tier with that shape
61
+ {"reroll": true} -> every unfrozen object takes its next alternative
62
+ Reroll walks the pool (the old choice goes to the back), so repeated rerolls
63
+ visit every alternative before repeating, independent of the seed.
64
  """
65
  by_id = {i["id"]: i for i in ledger["items"]}
 
66
  for ch in changes:
67
  if ch.get("reroll"):
68
  for it in ledger["items"]:
69
  dec = it.get("decision")
70
  if it["kind"] == "object" and not it["frozen"] and dec["alternatives"]:
71
+ dec["alternatives"].append(dec["chosen"])
72
+ dec["chosen"] = dec["alternatives"].pop(0)
73
  continue
74
  it = by_id.get(ch.get("id"))
75
  if it is None:
 
86
  k = int(ch["use"])
87
  dec["alternatives"][k], dec["chosen"] = dec["chosen"], dec["alternatives"][k]
88
  if ch.get("reject"):
89
+ nxt = next_tier(dec, it["word"])
90
  dec["rejected_tiers"].append(dec["tier"])
91
+ dec.update(nxt)
92
  if it["height_m"]["source"] != "user":
93
  it["height_m"] = {"value": None, "source": "pending"}
94
  if "shape" in ch:
vli/simplify.py CHANGED
@@ -24,10 +24,10 @@ def base_color(mesh):
24
 
25
  def decimate(mesh, faces):
26
  color = base_color(mesh)
27
- try:
28
- out = mesh.simplify_quadric_decimation(face_count=faces)
29
- except TypeError: # older trimesh signature
30
- out = mesh.simplify_quadric_decimation(faces)
31
  out.visual = trimesh.visual.ColorVisuals(out, face_colors=np.tile(color, (len(out.faces), 1)))
32
  return out
33
 
 
24
 
25
  def decimate(mesh, faces):
26
  color = base_color(mesh)
27
+ # fast-simplification rejects read-only arrays (cached views), so hand it fresh copies
28
+ src = trimesh.Trimesh(np.array(mesh.vertices, dtype=np.float64), np.array(mesh.faces, dtype=np.int64),
29
+ process=False)
30
+ out = src.simplify_quadric_decimation(face_count=faces)
31
  out.visual = trimesh.visual.ColorVisuals(out, face_colors=np.tile(color, (len(out.faces), 1)))
32
  return out
33