multimodalart HF Staff commited on
Commit
147e6e2
·
verified ·
1 Parent(s): 45f9134

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ examples/bathroom_shelf.png filter=lfs diff=lfs merge=lfs -text
37
+ examples/kitchen_counter.png filter=lfs diff=lfs merge=lfs -text
38
+ examples/robot_table.png filter=lfs diff=lfs merge=lfs -text
39
+ examples/shelf_mugs.png filter=lfs diff=lfs merge=lfs -text
40
+ examples/so101_pickplace.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,13 +1,36 @@
1
  ---
2
- title: Physbrain1 5 8b Demo
3
- emoji: 🚀
4
- colorFrom: pink
5
- colorTo: red
6
  sdk: gradio
7
  sdk_version: 6.26.0
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: PhysBrain 1.5-8B
3
+ emoji: 🦾
4
+ colorFrom: indigo
5
+ colorTo: blue
6
  sdk: gradio
7
  sdk_version: 6.26.0
 
8
  app_file: app.py
9
+ short_description: Embodied spatial reasoning, pointing and arm traces
10
+ python_version: "3.12"
11
+ startup_duration_timeout: 1h
12
  ---
13
 
14
+ # PhysBrain 1.5-8B
15
+
16
+ Demo for [DeepCybo/PhysBrain1.5-8B](https://huggingface.co/DeepCybo/PhysBrain1.5-8B),
17
+ an embodied vision-language model built on Qwen3-VL-8B-Instruct.
18
+
19
+ Three modes:
20
+
21
+ - **Spatial Q&A** — open-ended embodied questions about a scene.
22
+ - **Point / affordance** — referring expressions and free-space queries; points are
23
+ overlaid on the image.
24
+ - **Manipulation trace** — the 2D trajectory a robot arm should follow to complete a task.
25
+
26
+ Point / trajectory prompting follows the authors' evaluation harness
27
+ ([EmbodiedEvalKit](https://github.com/pickxiguapi/EmbodiedEvalKit), MIT): the model is asked for
28
+ `[{"point_2d": [x, y]}]` and coordinates are decoded from a 0–1000 normalised canvas.
29
+
30
+ The checkpoint also predicts action-chunk tokens and future-state VQ tokens; decoding
31
+ those requires the action codebook / VQ image decoder, which are not published, so this
32
+ demo exposes the embodied-understanding capabilities.
33
+
34
+ Example scenes come from [BAAI/RefSpatial-Bench](https://huggingface.co/datasets/BAAI/RefSpatial-Bench)
35
+ (Apache-2.0) and [lerobot/svla_so101_pickplace](https://huggingface.co/datasets/lerobot/svla_so101_pickplace)
36
+ (Apache-2.0).
app.py ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ast
2
+ import json
3
+ import os
4
+ import re
5
+ import time
6
+
7
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
8
+
9
+ import spaces # noqa: E402 (must precede torch)
10
+ import torch # noqa: E402
11
+ import gradio as gr # noqa: E402
12
+ from PIL import Image, ImageDraw # noqa: E402
13
+ from transformers import AutoModelForImageTextToText, AutoProcessor # noqa: E402
14
+
15
+ MODEL_ID = "DeepCybo/PhysBrain1.5-8B"
16
+
17
+ # Prompt suffix used by the authors' evaluation harness (EmbodiedEvalKit) for
18
+ # Qwen3-VL-backboned models. Coordinates come back normalised to 0-1000.
19
+ POINT_SUFFIX = 'The answer should be presented in JSON format as follows: [{"point_2d": [x, y]}].'
20
+
21
+ MODE_ASK = "Spatial Q&A"
22
+ MODE_POINT = "Point / affordance"
23
+ MODE_TRACE = "Manipulation trace"
24
+ MODES = [MODE_ASK, MODE_POINT, MODE_TRACE]
25
+
26
+ # Cap the number of visual tokens so a 4K upload doesn't explode the context.
27
+ MAX_PIXELS = 1024 * 1024
28
+
29
+ processor = AutoProcessor.from_pretrained(MODEL_ID)
30
+ model = AutoModelForImageTextToText.from_pretrained(
31
+ MODEL_ID,
32
+ dtype=torch.bfloat16,
33
+ attn_implementation="sdpa",
34
+ ).to("cuda")
35
+ model.eval()
36
+
37
+
38
+ # --------------------------------------------------------------------------- #
39
+ # point parsing / drawing
40
+ # --------------------------------------------------------------------------- #
41
+ def _walk(data):
42
+ """Recursively pull [x, y] pairs out of a parsed JSON/py structure."""
43
+ pts = []
44
+ if isinstance(data, dict):
45
+ for key in ("point_2d", "points", "point", "coordinates", "position"):
46
+ if key in data:
47
+ return _walk(data[key])
48
+ return []
49
+ if isinstance(data, (list, tuple)):
50
+ if len(data) == 2 and all(isinstance(v, (int, float)) for v in data):
51
+ return [[float(data[0]), float(data[1])]]
52
+ for item in data:
53
+ pts.extend(_walk(item))
54
+ return pts
55
+
56
+
57
+ def parse_points(text: str):
58
+ """Extract 2D points from a PhysBrain response.
59
+
60
+ Handles ``[{"point_2d": [x, y]}, ...]``, bare ``[[x, y], ...]``, markdown
61
+ code fences and points embedded in prose.
62
+ """
63
+ if not text or not text.strip():
64
+ return []
65
+
66
+ cleaned = re.sub(r"```(?:json|python)?\n?(.*?)\n?```", r"\1", text, flags=re.DOTALL)
67
+
68
+ # Try every bracketed blob, longest first (the full list beats a single pair).
69
+ blobs = re.findall(r"\[.*?\]\s*(?=$|[,\n]|\Z)", cleaned, flags=re.DOTALL)
70
+ blobs.sort(key=len, reverse=True)
71
+ for blob in blobs[:5]:
72
+ for loader in (json.loads, ast.literal_eval):
73
+ try:
74
+ pts = _walk(loader(blob))
75
+ except Exception:
76
+ continue
77
+ if pts:
78
+ return pts
79
+
80
+ # Last resort: raw "(x, y)" / "[x, y]" pairs anywhere in the text.
81
+ pairs = re.findall(r"[\[\(]\s*(-?\d+\.?\d*)\s*,\s*(-?\d+\.?\d*)\s*[\]\)]", cleaned)
82
+ return [[float(a), float(b)] for a, b in pairs]
83
+
84
+
85
+ def to_pixels(points, width: int, height: int):
86
+ """PhysBrain emits coordinates normalised to a 0-1000 canvas."""
87
+ out = []
88
+ for pt in points:
89
+ x = float(pt[0]) / 1000.0 * width
90
+ y = float(pt[1]) / 1000.0 * height
91
+ out.append((max(0.0, min(width - 1.0, x)), max(0.0, min(height - 1.0, y))))
92
+ return out
93
+
94
+
95
+ def _radius(image: Image.Image) -> int:
96
+ return max(5, int(0.011 * max(image.size)))
97
+
98
+
99
+ def draw_points(image: Image.Image, pixels):
100
+ canvas = image.convert("RGB").copy()
101
+ draw = ImageDraw.Draw(canvas, "RGBA")
102
+ r = _radius(canvas)
103
+ for idx, (x, y) in enumerate(pixels):
104
+ draw.ellipse([x - r * 2.2, y - r * 2.2, x + r * 2.2, y + r * 2.2],
105
+ fill=(255, 87, 34, 60))
106
+ draw.ellipse([x - r, y - r, x + r, y + r],
107
+ fill=(255, 87, 34, 255), outline=(255, 255, 255, 255),
108
+ width=max(2, r // 3))
109
+ if len(pixels) > 1:
110
+ draw.text((x + r * 1.6, y - r * 1.6), str(idx + 1),
111
+ fill=(255, 255, 255, 255), stroke_width=3,
112
+ stroke_fill=(0, 0, 0, 200))
113
+ return canvas
114
+
115
+
116
+ def draw_trace(image: Image.Image, pixels):
117
+ canvas = image.convert("RGB").copy()
118
+ draw = ImageDraw.Draw(canvas, "RGBA")
119
+ r = _radius(canvas)
120
+ n = len(pixels)
121
+ if n > 1:
122
+ draw.line(pixels, fill=(0, 0, 0, 130), width=max(5, r), joint="curve")
123
+ for i in range(n - 1):
124
+ t = i / max(1, n - 2)
125
+ colour = (int(60 + 195 * t), int(220 - 130 * t), int(255 - 200 * t), 255)
126
+ draw.line([pixels[i], pixels[i + 1]], fill=colour,
127
+ width=max(3, r - 2), joint="curve")
128
+ for idx, (x, y) in enumerate(pixels):
129
+ t = idx / max(1, n - 1)
130
+ colour = (int(60 + 195 * t), int(220 - 130 * t), int(255 - 200 * t), 255)
131
+ rr = r * (1.4 if idx in (0, n - 1) else 0.9)
132
+ draw.ellipse([x - rr, y - rr, x + rr, y + rr], fill=colour,
133
+ outline=(255, 255, 255, 255), width=2)
134
+ return canvas
135
+
136
+
137
+ # --------------------------------------------------------------------------- #
138
+ # prompt construction
139
+ # --------------------------------------------------------------------------- #
140
+ def build_prompt(instruction: str, mode: str, num_points: int) -> str:
141
+ instruction = (instruction or "").strip()
142
+ if mode == MODE_POINT:
143
+ return f"{instruction}\n{POINT_SUFFIX}"
144
+ if mode == MODE_TRACE:
145
+ return (
146
+ "You are currently a robot performing robotic manipulation tasks. "
147
+ f"The task instruction is: {instruction}. Use 2D points to trace the "
148
+ "movement trajectory of the robotic arm (not the manipulated object) "
149
+ "as it moves to complete the task. You must provide the points in the "
150
+ "order of the trajectory, and the number of points must be "
151
+ f"{int(num_points)}.\n{POINT_SUFFIX}"
152
+ )
153
+ return instruction
154
+
155
+
156
+ def prep_image(image: Image.Image) -> Image.Image:
157
+ image = image.convert("RGB")
158
+ w, h = image.size
159
+ if w * h > MAX_PIXELS:
160
+ scale = (MAX_PIXELS / (w * h)) ** 0.5
161
+ image = image.resize((max(32, int(w * scale)), max(32, int(h * scale))),
162
+ Image.LANCZOS)
163
+ return image
164
+
165
+
166
+ # --------------------------------------------------------------------------- #
167
+ # inference
168
+ # --------------------------------------------------------------------------- #
169
+ @spaces.GPU(duration=90)
170
+ def run(
171
+ image: Image.Image,
172
+ instruction: str,
173
+ mode: str = MODE_POINT,
174
+ num_points: int = 8,
175
+ max_new_tokens: int = 512,
176
+ temperature: float = 0.0,
177
+ progress=gr.Progress(track_tqdm=True),
178
+ ):
179
+ """Run PhysBrain 1.5-8B on an image plus an embodied instruction.
180
+
181
+ Args:
182
+ image: the scene the robot observes.
183
+ instruction: question, referring expression, or manipulation task.
184
+ mode: "Spatial Q&A", "Point / affordance", or "Manipulation trace".
185
+ num_points: number of waypoints requested in "Manipulation trace" mode.
186
+ max_new_tokens: generation length cap.
187
+ temperature: 0 for greedy decoding (what the authors evaluate with).
188
+
189
+ Returns:
190
+ The input image with any predicted points/trajectory overlaid, and the
191
+ raw model response.
192
+ """
193
+ if image is None:
194
+ raise gr.Error("Please provide an image.")
195
+ if not (instruction or "").strip():
196
+ raise gr.Error("Please provide an instruction or question.")
197
+
198
+ mode = mode if mode in MODES else MODE_POINT
199
+ original = image.convert("RGB")
200
+ vision_image = prep_image(original)
201
+ prompt = build_prompt(instruction, mode, num_points)
202
+
203
+ messages = [{
204
+ "role": "user",
205
+ "content": [{"type": "image", "image": vision_image},
206
+ {"type": "text", "text": prompt}],
207
+ }]
208
+
209
+ inputs = processor.apply_chat_template(
210
+ messages,
211
+ tokenize=True,
212
+ add_generation_prompt=True,
213
+ return_dict=True,
214
+ return_tensors="pt",
215
+ ).to(model.device)
216
+
217
+ gen_kwargs = dict(max_new_tokens=int(max_new_tokens), do_sample=temperature > 0)
218
+ if temperature > 0:
219
+ gen_kwargs.update(temperature=float(temperature), top_p=0.9)
220
+
221
+ t0 = time.perf_counter()
222
+ with torch.inference_mode():
223
+ generated = model.generate(**inputs, **gen_kwargs)
224
+ trimmed = generated[:, inputs["input_ids"].shape[1]:]
225
+ answer = processor.batch_decode(trimmed, skip_special_tokens=True)[0].strip()
226
+ elapsed = time.perf_counter() - t0
227
+ print(f"[physbrain] mode={mode} vision={vision_image.size} "
228
+ f"grid={inputs.get('image_grid_thw')} {elapsed:.1f}s", flush=True)
229
+
230
+ overlay = original
231
+ points = parse_points(answer) if mode in (MODE_POINT, MODE_TRACE) else []
232
+ if points:
233
+ pixels = to_pixels(points, *original.size)
234
+ overlay = draw_trace(original, pixels) if mode == MODE_TRACE else draw_points(original, pixels)
235
+
236
+ return overlay, answer
237
+
238
+
239
+ # --------------------------------------------------------------------------- #
240
+ # UI
241
+ # --------------------------------------------------------------------------- #
242
+ CSS = """
243
+ #col-container { max-width: 1200px; margin: 0 auto; }
244
+ .dark .gradio-container { color: var(--body-text-color); }
245
+ """
246
+
247
+ EXAMPLES = [
248
+ [
249
+ "examples/shelf_mugs.png",
250
+ "Please point out the orange box.",
251
+ MODE_POINT,
252
+ ],
253
+ [
254
+ "examples/kitchen_counter.png",
255
+ "Please point out the third object from left to right on the closest platform.",
256
+ MODE_POINT,
257
+ ],
258
+ [
259
+ "examples/bathroom_shelf.png",
260
+ "Please point out the free space on the second shelf of the wooden shelf.",
261
+ MODE_POINT,
262
+ ],
263
+ [
264
+ "examples/so101_pickplace.png",
265
+ "put the pink lego brick into the transparent box",
266
+ MODE_TRACE,
267
+ ],
268
+ [
269
+ "examples/robot_table.png",
270
+ "The robot arm has to pick up the yellow mustard bottle. Where is it "
271
+ "relative to the bowl, and what should the arm do first?",
272
+ MODE_ASK,
273
+ ],
274
+ ]
275
+
276
+ with gr.Blocks(theme=gr.themes.Citrus(), css=CSS, title="PhysBrain 1.5-8B") as demo:
277
+ with gr.Column(elem_id="col-container"):
278
+ gr.Markdown(
279
+ """
280
+ # PhysBrain 1.5-8B — embodied spatial reasoning
281
+
282
+ [PhysBrain 1.5-8B](https://huggingface.co/DeepCybo/PhysBrain1.5-8B) is DeepCybo's
283
+ embodied vision-language model (a Qwen3-VL-8B finetune). Give it a scene and ask a
284
+ spatial question, ask it to **point** at a target or a free space to place something,
285
+ or ask it to **trace** the trajectory a robot arm should follow.
286
+ """
287
+ )
288
+
289
+ with gr.Row():
290
+ with gr.Column(scale=1):
291
+ image_in = gr.Image(label="Scene", type="pil", height=340)
292
+ mode_in = gr.Radio(
293
+ MODES, value=MODE_POINT, label="Task",
294
+ info="Point / trace answers are overlaid on the image.",
295
+ )
296
+ instruction_in = gr.Textbox(
297
+ label="Instruction",
298
+ placeholder="e.g. Please point out the free space to the left of the mug.",
299
+ lines=3,
300
+ )
301
+ run_btn = gr.Button("Run", variant="primary")
302
+ with gr.Column(scale=1):
303
+ image_out = gr.Image(label="Prediction", type="pil", height=340)
304
+ text_out = gr.Textbox(label="Model response", lines=8, show_copy_button=True)
305
+
306
+ with gr.Accordion("Advanced settings", open=False):
307
+ num_points_in = gr.Slider(2, 32, value=8, step=1,
308
+ label="Trajectory waypoints (trace mode)")
309
+ max_tokens_in = gr.Slider(64, 2048, value=512, step=64,
310
+ label="Max new tokens")
311
+ temperature_in = gr.Slider(0.0, 1.0, value=0.0, step=0.05,
312
+ label="Temperature (0 = greedy)")
313
+
314
+ gr.Examples(
315
+ examples=EXAMPLES,
316
+ inputs=[image_in, instruction_in, mode_in],
317
+ outputs=[image_out, text_out],
318
+ fn=run,
319
+ cache_examples=True,
320
+ cache_mode="lazy",
321
+ label="Examples",
322
+ )
323
+
324
+ gr.Markdown(
325
+ """
326
+ Point coordinates follow the authors' evaluation format
327
+ (`[{"point_2d": [x, y]}]`, normalised to a 0–1000 canvas). The model also has
328
+ action-chunk and future-state token heads; those need the unreleased action /
329
+ VQ detokenizers, so this demo covers the embodied-understanding side.
330
+
331
+ Example scenes: [BAAI/RefSpatial-Bench](https://huggingface.co/datasets/BAAI/RefSpatial-Bench)
332
+ and [lerobot/svla_so101_pickplace](https://huggingface.co/datasets/lerobot/svla_so101_pickplace) (both Apache-2.0).
333
+ """
334
+ )
335
+
336
+ run_btn.click(
337
+ run,
338
+ inputs=[image_in, instruction_in, mode_in, num_points_in, max_tokens_in, temperature_in],
339
+ outputs=[image_out, text_out],
340
+ api_name="predict",
341
+ )
342
+
343
+ demo.queue().launch(mcp_server=True)
examples/bathroom_shelf.png ADDED

Git LFS Details

  • SHA256: f58507634746672b23a61e70e39a8d12f5f30c4f5b461fda06de4e8c39051b39
  • Pointer size: 131 Bytes
  • Size of remote file: 325 kB
examples/kitchen_counter.png ADDED

Git LFS Details

  • SHA256: 34c7753544ad58df1d7472c1e4cc6d13e3c09c87f06f2981b84ce5fe43b5996f
  • Pointer size: 131 Bytes
  • Size of remote file: 371 kB
examples/robot_table.png ADDED

Git LFS Details

  • SHA256: b9a39283f37a7e6594b5390c4533bb7651d47dd48c52c3c9968ad42e8f66ef4f
  • Pointer size: 131 Bytes
  • Size of remote file: 404 kB
examples/shelf_mugs.png ADDED

Git LFS Details

  • SHA256: 93cba22162b77b3f7532b979e377107dde90afd5572ae52ea2fa6efc1e36e474
  • Pointer size: 131 Bytes
  • Size of remote file: 508 kB
examples/so101_pickplace.png ADDED

Git LFS Details

  • SHA256: 07755215c58204de7d37297460c6150a06330dca9730f015f7d8a09a3e52f552
  • Pointer size: 131 Bytes
  • Size of remote file: 109 kB
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ transformers>=5.12.1
2
+ accelerate
3
+ torchvision
4
+ pillow
5
+ numpy