Spaces:
Running on Zero
Running on Zero
kwabs22 Claude Opus 5.5 commited on
Commit ·
d7aa77a
1
Parent(s): bbe0bad
Show datasets under the title; vision calls use the caller's token
Browse filesThe public Space stores no token. Back-translation and recognition run
on the signed-in user's account (HF OAuth, inference-api scope) or an
MCP caller's Bearer token, so the owner's credits are never spent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README.md
CHANGED
|
@@ -8,6 +8,9 @@ sdk_version: 6.27.0
|
|
| 8 |
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
|
|
|
|
|
|
|
|
|
| 11 |
short_description: Visualisation and speed of input attempt
|
| 12 |
---
|
| 13 |
|
|
|
|
| 8 |
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
+
hf_oauth: true
|
| 12 |
+
hf_oauth_scopes:
|
| 13 |
+
- inference-api
|
| 14 |
short_description: Visualisation and speed of input attempt
|
| 15 |
---
|
| 16 |
|
SPEC.md
CHANGED
|
@@ -101,7 +101,10 @@ Ladder rungs: original → 20k / 5k / 1k / 250 / 60 faces → convex hull → or
|
|
| 101 |
box. With `check_recognition`, a vision model names each rung, and
|
| 102 |
`simplest_recognized` is the lowest rung that still reads as the word.
|
| 103 |
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
## Known gaps / open questions
|
| 107 |
|
|
|
|
| 101 |
box. With `check_recognition`, a vision model names each rung, and
|
| 102 |
`simplest_recognized` is the lowest rung that still reads as the word.
|
| 103 |
|
| 104 |
+
**No secrets.** The Space is public, so it stores no token. Datasets are public
|
| 105 |
+
and download without one. Vision-model calls (back-translation, recognition) use
|
| 106 |
+
the **caller's** token: HF sign-in in the UI (`hf_oauth`, `inference-api` scope),
|
| 107 |
+
or a Bearer `Authorization` header from MCP clients. Optional env: `VLM_MODEL`, `VLI_CACHE`.
|
| 108 |
|
| 109 |
## Known gaps / open questions
|
| 110 |
|
app.py
CHANGED
|
@@ -35,6 +35,14 @@ def _json(text, name, default=None):
|
|
| 35 |
raise ValueError(f"{name} is not valid JSON")
|
| 36 |
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
def _seed(value):
|
| 39 |
"""Seeds arrive as strings (some MCP clients can't send numbers)."""
|
| 40 |
try:
|
|
@@ -129,7 +137,8 @@ def refine_scene(ledger_json: str, changes_json: str = "[]", seed: str = "0") ->
|
|
| 129 |
_fail(e)
|
| 130 |
|
| 131 |
|
| 132 |
-
def simplify_item(ledger_json: str, item_id: str, check_recognition: bool = False
|
|
|
|
| 133 |
"""Build the simplification ladder for one object: full asset -> fewer faces -> convex hull -> box.
|
| 134 |
|
| 135 |
With check_recognition, a vision model names each rung's picture; the lowest rung
|
|
@@ -138,12 +147,16 @@ def simplify_item(ledger_json: str, item_id: str, check_recognition: bool = Fals
|
|
| 138 |
Args:
|
| 139 |
ledger_json: Ledger JSON from translate_to_scene or refine_scene.
|
| 140 |
item_id: The object item to simplify, e.g. "n1".
|
| 141 |
-
check_recognition: Ask a vision model to name each rung (
|
|
|
|
| 142 |
|
| 143 |
Returns:
|
| 144 |
(report JSON string, gallery of rung pictures, GLB file per rung).
|
| 145 |
"""
|
| 146 |
try:
|
|
|
|
|
|
|
|
|
|
| 147 |
ledger = _json(ledger_json, "ledger_json")
|
| 148 |
item = next((i for i in ledger["items"] if i["id"] == item_id), None)
|
| 149 |
if item is None or item["kind"] != "object":
|
|
@@ -158,7 +171,7 @@ def simplify_item(ledger_json: str, item_id: str, check_recognition: bool = Fals
|
|
| 158 |
rung.export(glb)
|
| 159 |
entry = {"rung": label, **simplify.stats(rung)}
|
| 160 |
if check_recognition:
|
| 161 |
-
answer = vlm.name_object(png)
|
| 162 |
entry["named_as"] = answer
|
| 163 |
entry["recognized"] = item["word"].lower() in answer.lower()
|
| 164 |
report["rungs"].append(entry)
|
|
@@ -172,10 +185,12 @@ def simplify_item(ledger_json: str, item_id: str, check_recognition: bool = Fals
|
|
| 172 |
_fail(e)
|
| 173 |
|
| 174 |
|
| 175 |
-
def describe_scene(ledger_json: str, seed: str = "0"
|
|
|
|
| 176 |
"""Back-translate: render the ledger's scene and have a vision model describe it in English.
|
| 177 |
|
| 178 |
Comparing the description with the original sentence checks the translation.
|
|
|
|
| 179 |
|
| 180 |
Args:
|
| 181 |
ledger_json: Ledger JSON from translate_to_scene or refine_scene.
|
|
@@ -185,11 +200,14 @@ def describe_scene(ledger_json: str, seed: str = "0") -> str:
|
|
| 185 |
JSON string: {"original", "back_translation", "nouns_recovered": {word: bool}}.
|
| 186 |
"""
|
| 187 |
try:
|
|
|
|
|
|
|
|
|
|
| 188 |
ledger = _json(ledger_json, "ledger_json")
|
| 189 |
meshes = pipeline.settle_all(ledger)
|
| 190 |
config = scene.configure(ledger, meshes, _seed(seed))
|
| 191 |
png = scene.render_scene(ledger, meshes, config, os.path.join(tempfile.mkdtemp(), "scene.png"))
|
| 192 |
-
text = vlm.describe(png)
|
| 193 |
nouns = [i["word"] for i in ledger["items"] if i["kind"] == "object"]
|
| 194 |
return json.dumps({"original": ledger["sentence"], "back_translation": text,
|
| 195 |
"nouns_recovered": {n: n in text.lower() for n in nouns}}, indent=1)
|
|
@@ -234,9 +252,26 @@ def _use_primitive(primitives_json, word, shape):
|
|
| 234 |
|
| 235 |
EXAMPLE_CHANGES = '[\n {"id": "n1", "freeze": true},\n {"reroll": true}\n]'
|
| 236 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
with gr.Blocks(title="VisualLearningIdea") as demo:
|
| 238 |
gr.Markdown("# English → 3D\nType with the words the 3D dictionary has. **Bold** = an object asset; "
|
| 239 |
"verbs and prepositions are relations between meshes.")
|
|
|
|
|
|
|
| 240 |
with gr.Row():
|
| 241 |
sentence = gr.Textbox(label="Sentence", placeholder="the cat sat and the sun shone", scale=4)
|
| 242 |
seed = gr.Textbox(label="Configuration seed", value="0", scale=1)
|
|
|
|
| 35 |
raise ValueError(f"{name} is not valid JSON")
|
| 36 |
|
| 37 |
|
| 38 |
+
def _user_token(oauth_token, request):
|
| 39 |
+
"""The caller's own token: from HF sign-in (UI) or a Bearer header (MCP). Never the Space's."""
|
| 40 |
+
if oauth_token is not None and oauth_token.token:
|
| 41 |
+
return oauth_token.token
|
| 42 |
+
auth = (request.headers.get("authorization", "") if request is not None else "")
|
| 43 |
+
return auth[7:].strip() if auth.lower().startswith("bearer ") else None
|
| 44 |
+
|
| 45 |
+
|
| 46 |
def _seed(value):
|
| 47 |
"""Seeds arrive as strings (some MCP clients can't send numbers)."""
|
| 48 |
try:
|
|
|
|
| 137 |
_fail(e)
|
| 138 |
|
| 139 |
|
| 140 |
+
def simplify_item(ledger_json: str, item_id: str, check_recognition: bool = False,
|
| 141 |
+
oauth_token: gr.OAuthToken | None = None, request: gr.Request = None) -> tuple[str, list, list]:
|
| 142 |
"""Build the simplification ladder for one object: full asset -> fewer faces -> convex hull -> box.
|
| 143 |
|
| 144 |
With check_recognition, a vision model names each rung's picture; the lowest rung
|
|
|
|
| 147 |
Args:
|
| 148 |
ledger_json: Ledger JSON from translate_to_scene or refine_scene.
|
| 149 |
item_id: The object item to simplify, e.g. "n1".
|
| 150 |
+
check_recognition: Ask a vision model to name each rung (slower; runs on the caller's
|
| 151 |
+
own HF account, so it needs sign-in or a Bearer token).
|
| 152 |
|
| 153 |
Returns:
|
| 154 |
(report JSON string, gallery of rung pictures, GLB file per rung).
|
| 155 |
"""
|
| 156 |
try:
|
| 157 |
+
token = _user_token(oauth_token, request) if check_recognition else None
|
| 158 |
+
if check_recognition and not token:
|
| 159 |
+
raise ValueError(vlm.SIGN_IN) # before any heavy work
|
| 160 |
ledger = _json(ledger_json, "ledger_json")
|
| 161 |
item = next((i for i in ledger["items"] if i["id"] == item_id), None)
|
| 162 |
if item is None or item["kind"] != "object":
|
|
|
|
| 171 |
rung.export(glb)
|
| 172 |
entry = {"rung": label, **simplify.stats(rung)}
|
| 173 |
if check_recognition:
|
| 174 |
+
answer = vlm.name_object(png, token)
|
| 175 |
entry["named_as"] = answer
|
| 176 |
entry["recognized"] = item["word"].lower() in answer.lower()
|
| 177 |
report["rungs"].append(entry)
|
|
|
|
| 185 |
_fail(e)
|
| 186 |
|
| 187 |
|
| 188 |
+
def describe_scene(ledger_json: str, seed: str = "0",
|
| 189 |
+
oauth_token: gr.OAuthToken | None = None, request: gr.Request = None) -> str:
|
| 190 |
"""Back-translate: render the ledger's scene and have a vision model describe it in English.
|
| 191 |
|
| 192 |
Comparing the description with the original sentence checks the translation.
|
| 193 |
+
The vision model runs on the caller's own HF account (sign-in or Bearer token).
|
| 194 |
|
| 195 |
Args:
|
| 196 |
ledger_json: Ledger JSON from translate_to_scene or refine_scene.
|
|
|
|
| 200 |
JSON string: {"original", "back_translation", "nouns_recovered": {word: bool}}.
|
| 201 |
"""
|
| 202 |
try:
|
| 203 |
+
token = _user_token(oauth_token, request)
|
| 204 |
+
if not token:
|
| 205 |
+
raise ValueError(vlm.SIGN_IN) # before any heavy work
|
| 206 |
ledger = _json(ledger_json, "ledger_json")
|
| 207 |
meshes = pipeline.settle_all(ledger)
|
| 208 |
config = scene.configure(ledger, meshes, _seed(seed))
|
| 209 |
png = scene.render_scene(ledger, meshes, config, os.path.join(tempfile.mkdtemp(), "scene.png"))
|
| 210 |
+
text = vlm.describe(png, token)
|
| 211 |
nouns = [i["word"] for i in ledger["items"] if i["kind"] == "object"]
|
| 212 |
return json.dumps({"original": ledger["sentence"], "back_translation": text,
|
| 213 |
"nouns_recovered": {n: n in text.lower() for n in nouns}}, indent=1)
|
|
|
|
| 252 |
|
| 253 |
EXAMPLE_CHANGES = '[\n {"id": "n1", "freeze": true},\n {"reroll": true}\n]'
|
| 254 |
|
| 255 |
+
DATASETS_MD = """
|
| 256 |
+
**Dictionary (public datasets, read-only):**
|
| 257 |
+
1. [NVIDIA SimReady](https://github.com/NVIDIA-Omniverse/simready-central): metric OpenUSD assets,
|
| 258 |
+
e.g. [nvidia/simready-assets](https://huggingface.co/datasets/nvidia/simready-assets). License per package
|
| 259 |
+
(e.g. CC-BY-4.0). Only geometry and the thumbnail are downloaded, not the ~1 GB of textures.
|
| 260 |
+
2. [allenai/objaverse](https://huggingface.co/datasets/allenai/objaverse): the LVIS-labelled subset.
|
| 261 |
+
License per object; non-commercial (NC) and no-derivatives (ND) objects are skipped.
|
| 262 |
+
3. Missing words: a primitive you choose (box, sphere, cylinder, cone, plane). Generation comes later.
|
| 263 |
+
|
| 264 |
+
**Why sign in?** Building scenes needs no account: the datasets are public. Only *Back-translation* and
|
| 265 |
+
*Check recognition* call a vision model ([Qwen2.5-VL](https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct)
|
| 266 |
+
via HF Inference), and those calls run on **your own** Hugging Face account. This Space stores no token,
|
| 267 |
+
so nobody's credits are spent but the caller's.
|
| 268 |
+
"""
|
| 269 |
+
|
| 270 |
with gr.Blocks(title="VisualLearningIdea") as demo:
|
| 271 |
gr.Markdown("# English → 3D\nType with the words the 3D dictionary has. **Bold** = an object asset; "
|
| 272 |
"verbs and prepositions are relations between meshes.")
|
| 273 |
+
gr.Markdown(DATASETS_MD)
|
| 274 |
+
gr.LoginButton()
|
| 275 |
with gr.Row():
|
| 276 |
sentence = gr.Textbox(label="Sentence", placeholder="the cat sat and the sun shone", scale=4)
|
| 277 |
seed = gr.Textbox(label="Configuration seed", value="0", scale=1)
|
vli/vlm.py
CHANGED
|
@@ -1,16 +1,24 @@
|
|
| 1 |
-
"""Back-translation: a vision model reads the picture back into English.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import base64
|
| 3 |
import os
|
| 4 |
|
| 5 |
from huggingface_hub import InferenceClient
|
| 6 |
|
| 7 |
VLM_MODEL = os.environ.get("VLM_MODEL", "Qwen/Qwen2.5-VL-7B-Instruct")
|
|
|
|
|
|
|
| 8 |
|
| 9 |
|
| 10 |
-
def ask(image_path, prompt, max_tokens=80):
|
|
|
|
|
|
|
| 11 |
with open(image_path, "rb") as f:
|
| 12 |
data = base64.b64encode(f.read()).decode()
|
| 13 |
-
client = InferenceClient(token=
|
| 14 |
resp = client.chat_completion(
|
| 15 |
model=VLM_MODEL, max_tokens=max_tokens,
|
| 16 |
messages=[{"role": "user", "content": [
|
|
@@ -19,9 +27,9 @@ def ask(image_path, prompt, max_tokens=80):
|
|
| 19 |
return resp.choices[0].message.content.strip()
|
| 20 |
|
| 21 |
|
| 22 |
-
def describe(image_path):
|
| 23 |
-
return ask(image_path, "Describe this 3D scene in one short plain sentence.")
|
| 24 |
|
| 25 |
|
| 26 |
-
def name_object(image_path):
|
| 27 |
-
return ask(image_path, "What single object is this? Answer with one or two words.", max_tokens=10)
|
|
|
|
| 1 |
+
"""Back-translation: a vision model reads the picture back into English.
|
| 2 |
+
|
| 3 |
+
Calls always use the *caller's* token, never one stored on the Space, so a
|
| 4 |
+
public Space can't spend its owner's inference credits.
|
| 5 |
+
"""
|
| 6 |
import base64
|
| 7 |
import os
|
| 8 |
|
| 9 |
from huggingface_hub import InferenceClient
|
| 10 |
|
| 11 |
VLM_MODEL = os.environ.get("VLM_MODEL", "Qwen/Qwen2.5-VL-7B-Instruct")
|
| 12 |
+
SIGN_IN = ("The vision model runs on your own Hugging Face account: sign in first "
|
| 13 |
+
"(MCP callers: send your HF token as a Bearer Authorization header).")
|
| 14 |
|
| 15 |
|
| 16 |
+
def ask(image_path, prompt, token, max_tokens=80):
|
| 17 |
+
if not token:
|
| 18 |
+
raise ValueError(SIGN_IN)
|
| 19 |
with open(image_path, "rb") as f:
|
| 20 |
data = base64.b64encode(f.read()).decode()
|
| 21 |
+
client = InferenceClient(token=token)
|
| 22 |
resp = client.chat_completion(
|
| 23 |
model=VLM_MODEL, max_tokens=max_tokens,
|
| 24 |
messages=[{"role": "user", "content": [
|
|
|
|
| 27 |
return resp.choices[0].message.content.strip()
|
| 28 |
|
| 29 |
|
| 30 |
+
def describe(image_path, token):
|
| 31 |
+
return ask(image_path, "Describe this 3D scene in one short plain sentence.", token)
|
| 32 |
|
| 33 |
|
| 34 |
+
def name_object(image_path, token):
|
| 35 |
+
return ask(image_path, "What single object is this? Answer with one or two words.", token, max_tokens=10)
|