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 files

The 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>

Files changed (4) hide show
  1. README.md +3 -0
  2. SPEC.md +4 -1
  3. app.py +40 -5
  4. vli/vlm.py +15 -7
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
- Secrets: `HF_TOKEN` (vision model calls). Optional env: `VLM_MODEL`, `VLI_CACHE`.
 
 
 
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) -> tuple[str, list, list]:
 
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 (needs HF_TOKEN; slower).
 
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") -> str:
 
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=os.environ.get("HF_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)