EdwinS9 commited on
Commit
007bdf0
·
1 Parent(s): f5e2df2

Add GVL robot-judge Gradio app and requirements

Browse files
Files changed (2) hide show
  1. app.py +170 -0
  2. requirements.txt +14 -0
app.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Build Small hackathon starter: sub-32B VLM as a training-free judge for robot manipulation.
3
+
4
+ What it does
5
+ ------------
6
+ Given a manipulation episode (a list of frames + a task description), it uses MiniCPM-V to
7
+ estimate per-frame task progress (0-100) using the GVL trick (shuffle frames so the model
8
+ can't just copy temporal order), then:
9
+ - plots the progress curve,
10
+ - derives a success verdict from late-stage progress,
11
+ - reports Value-Order Correlation (VOC) = rank corr between predicted progress and true
12
+ chronological order. High VOC means the model actually understands task progression.
13
+
14
+ Method reference: Ma et al., "Vision Language Models are In-Context Value Learners" (GVL),
15
+ arXiv 2411.04549. VOC and the shuffled-frame formulation are from that paper.
16
+
17
+ Notes
18
+ -----
19
+ - The MiniCPM-V call signature can change between versions. Verify against the current model
20
+ card at https://huggingface.co/openbmb/MiniCPM-V-4_5 before relying on it.
21
+ - Use MiniCPM-V-4.6 for local dev on 8GB VRAM, MiniCPM-V-4_5 (8B) on ZeroGPU/Modal
22
+ for the real numbers. The 4.5-vs-4.6 gap is your headline finding.
23
+ - This is a scaffold. The parts that matter (GVL scoring, VOC) are correct; the model I/O is
24
+ where you should expect to adjust.
25
+ """
26
+
27
+ import random
28
+ import numpy as np
29
+ from PIL import Image
30
+ from scipy.stats import spearmanr
31
+ import gradio as gr
32
+
33
+ # ZeroGPU: the decorator moves work onto a GPU slice on Spaces. Harmless to import locally.
34
+ try:
35
+ import spaces
36
+
37
+ GPU = spaces.GPU
38
+ except Exception: # local dev without the spaces package
39
+
40
+ def GPU(*a, **k):
41
+ def wrap(fn):
42
+ return fn
43
+
44
+ return wrap if (a and callable(a[0]) is False) else (a[0] if a else None)
45
+
46
+
47
+ import torch
48
+ from transformers import AutoModel, AutoTokenizer
49
+
50
+ MODEL_ID = "openbmb/MiniCPM-V-4_5" # swap to openbmb/MiniCPM-V-4.6 for the smaller local run
51
+ N_FRAMES = 16
52
+ SUCCESS_THRESHOLD = (
53
+ 70.0 # late-stage progress above this counts as success; tune on your data
54
+ )
55
+
56
+ _model = None
57
+ _tokenizer = None
58
+
59
+
60
+ def _load():
61
+ global _model, _tokenizer
62
+ if _model is None:
63
+ _tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)
64
+ _model = (
65
+ AutoModel.from_pretrained(
66
+ MODEL_ID,
67
+ trust_remote_code=True,
68
+ torch_dtype=torch.bfloat16,
69
+ attn_implementation="sdpa",
70
+ )
71
+ .eval()
72
+ .cuda()
73
+ )
74
+ return _model, _tokenizer
75
+
76
+
77
+ def sample_frames(frames, n=N_FRAMES):
78
+ """Uniformly sample n frames from a list of PIL images."""
79
+ if len(frames) <= n:
80
+ return list(range(len(frames))), frames
81
+ idx = np.linspace(0, len(frames) - 1, n).astype(int).tolist()
82
+ return idx, [frames[i] for i in idx]
83
+
84
+
85
+ def _ask_progress(model, tokenizer, task, init_frame, query_frame):
86
+ """One VLM call: given the initial frame as the 0% reference, score one query frame."""
87
+ prompt = (
88
+ f"A robot is performing this task: {task}.\n"
89
+ f"The first image is the initial state (0% complete).\n"
90
+ f"The second image is a frame from somewhere in the episode (order unknown).\n"
91
+ f"Estimate how complete the task is in the second image, as a single integer 0-100. "
92
+ f"Reply with the number only."
93
+ )
94
+ msgs = [{"role": "user", "content": [init_frame, query_frame, prompt]}]
95
+ out = model.chat(
96
+ image=None, msgs=msgs, tokenizer=tokenizer
97
+ ) # verify signature per model card
98
+ digits = "".join(c for c in str(out) if c.isdigit())
99
+ if not digits:
100
+ return 0.0
101
+ return float(max(0, min(100, int(digits[:3]))))
102
+
103
+
104
+ @GPU(duration=120)
105
+ def score_episode(frames, task):
106
+ """GVL-style scoring. Returns (chronological progress list, VOC)."""
107
+ model, tokenizer = _load()
108
+ _, sampled = sample_frames(frames)
109
+ init = sampled[0]
110
+ rest = list(enumerate(sampled))[1:] # (true_chrono_index, frame)
111
+
112
+ shuffled = rest[:]
113
+ random.shuffle(shuffled) # the GVL trick: break temporal adjacency
114
+
115
+ scored = {}
116
+ for true_idx, frame in shuffled:
117
+ scored[true_idx] = _ask_progress(model, tokenizer, task, init, frame)
118
+
119
+ progress = [0.0] + [scored[i] for i, _ in rest] # restore chronological order
120
+ true_order = list(range(len(progress)))
121
+ voc, _ = spearmanr(progress, true_order)
122
+ return progress, float(voc)
123
+
124
+
125
+ def run(video_frames_dir_or_list, task):
126
+ """Gradio entry. Expects a list of PIL frames; adapt to your loader (lerobot / decord)."""
127
+ frames = video_frames_dir_or_list # TODO: plug your LeRobot/LIBERO/OXE episode loader here
128
+ if not frames:
129
+ return None, "No frames loaded.", None
130
+ progress, voc = score_episode(frames, task)
131
+ late = np.mean(progress[-3:]) if len(progress) >= 3 else progress[-1]
132
+ verdict = "SUCCESS" if late >= SUCCESS_THRESHOLD else "FAILURE"
133
+
134
+ import matplotlib.pyplot as plt
135
+
136
+ fig, ax = plt.subplots(figsize=(6, 3))
137
+ ax.plot(range(len(progress)), progress, marker="o")
138
+ ax.set_xlabel("frame (chronological)")
139
+ ax.set_ylabel("predicted progress (%)")
140
+ ax.set_ylim(-5, 105)
141
+ ax.set_title(f"VOC = {voc:.2f}")
142
+ fig.tight_layout()
143
+
144
+ summary = f"Verdict: {verdict}\nLate-stage progress: {late:.0f}%\nVOC: {voc:.2f}"
145
+ return fig, summary, voc
146
+
147
+
148
+ with gr.Blocks(title="Can a small VLM judge robot success?") as demo:
149
+ gr.Markdown(
150
+ "## Small VLM as a training-free robot judge\n"
151
+ "Upload or pick a manipulation episode. The model estimates task progress per frame "
152
+ "(GVL shuffled-frame method) and reports VOC and a success verdict."
153
+ )
154
+ with gr.Row():
155
+ with gr.Column():
156
+ task_in = gr.Textbox(
157
+ label="Task description", value="put the red block in the bowl"
158
+ )
159
+ frames_in = gr.State(
160
+ []
161
+ ) # replace with a gr.File / example picker wired to your loader
162
+ run_btn = gr.Button("Judge episode", variant="primary")
163
+ with gr.Column():
164
+ plot_out = gr.Plot(label="Progress curve")
165
+ summary_out = gr.Textbox(label="Result")
166
+ voc_out = gr.Number(label="VOC")
167
+ run_btn.click(run, [frames_in, task_in], [plot_out, summary_out, voc_out])
168
+
169
+ if __name__ == "__main__":
170
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ZeroGPU images already include CUDA torch/torchvision, so do not pin them here.
2
+ # Match the transformers pin (and any extras like timm) to the MiniCPM-V model card
3
+ # at https://huggingface.co/openbmb/MiniCPM-V-4_5 to avoid trust_remote_code load errors.
4
+
5
+ gradio
6
+ spaces
7
+ transformers
8
+ accelerate
9
+ sentencepiece
10
+ pillow
11
+ scipy
12
+ numpy
13
+ matplotlib
14
+ decord