Spaces:
Running on Zero
Running on Zero
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>
- SPEC.md +2 -1
- app.py +9 -6
- vli/grammar.py +2 -1
- vli/pipeline.py +16 -10
- 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
|
|
|
|
| 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
|
| 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
|
|
|
|
| 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
|
|
|
|
| 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,
|
|
|
|
| 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", [])
|
| 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
|
| 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 |
-
|
|
|
|
| 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"]["
|
| 32 |
-
q["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
|
| 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
|
|
|
|
|
|
|
| 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 |
-
|
| 67 |
-
dec["
|
| 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(
|
| 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 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 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 |
|