File size: 5,702 Bytes
95ab6f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9c4819
95ab6f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9c4819
95ab6f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9c4819
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
import spaces  # MUST come before torch / transformers
import torch
import gradio as gr
from transformers import AutoProcessor, AutoModelForImageTextToText

MODEL_ID = "webbrain-one/webbrain-vl-2-450M"

# Production six-section system prompt from WebBrain's test/vision/prompt.mjs
SYSTEM_PROMPT = (
    "You are the vision subsystem of a web-automation agent. "
    "A screenshot of the current browser viewport is attached. "
    "Describe what is on screen so the planning agent can decide its next action.\n\n"
    "Format — keep it terse, structured, no flowery prose:\n\n"
    "1) Page purpose: one line (e.g. \"GitHub repo issue list\", "
    "\"Gmail compose\", \"Stripe checkout form\").\n"
    "2) Visible text: list the EXACT strings on buttons, links, headings, tabs, "
    "and menu items. Quote them verbatim. Do not paraphrase.\n"
    "3) Inputs: list each visible form field with its label, placeholder, "
    "current value, and whether it is focused/disabled.\n"
    "4) State signals: loading spinners, toasts, modals, error banners, "
    "success messages, CAPTCHAs, cookie/consent banners, overlays.\n"
    "5) Blockers: anything that would prevent the next likely action "
    "(overlay, disabled submit, missing data, auth prompt).\n"
    "6) Unknowns: if you cannot read something clearly, say so. "
    "Do not guess numbers, names, or identifiers.\n\n"
    "Rules: no prose intro, no conclusion, no \"this screenshot shows...\", "
    "no layout description unless it matters (e.g. \"left nav is collapsed\"). "
    "If the page is blank or still loading, say that in one line and stop."
)

USER_TEXT = (
    "Describe this screenshot of the current browser viewport "
    "for a web-automation agent. Follow the format in the system prompt."
)

processor = AutoProcessor.from_pretrained(MODEL_ID)
model = AutoModelForImageTextToText.from_pretrained(
    MODEL_ID,
    dtype=torch.bfloat16,
).to("cuda")


@spaces.GPU(duration=30)
def analyze_screenshot(image, max_new_tokens=800, temperature=0.0):
    """Analyze a browser screenshot and return a structured six-section observation.

    Args:
        image: A browser viewport screenshot (PIL Image).
        max_new_tokens: Maximum number of tokens to generate.
        temperature: Sampling temperature (0 = deterministic).
    """
    if image is None:
        return "Please upload a browser screenshot image."

    conversation = [
        {
            "role": "system",
            "content": [{"type": "text", "text": SYSTEM_PROMPT}],
        },
        {
            "role": "user",
            "content": [
                {"type": "image", "image": image},
                {"type": "text", "text": USER_TEXT},
            ],
        },
    ]

    inputs = processor.apply_chat_template(
        conversation,
        add_generation_prompt=True,
        return_tensors="pt",
        return_dict=True,
        tokenize=True,
    ).to("cuda")

    gen_kwargs = {
        "max_new_tokens": int(max_new_tokens),
    }
    if temperature > 0:
        gen_kwargs["temperature"] = float(temperature)
    else:
        gen_kwargs["do_sample"] = False

    with torch.no_grad():
        output_ids = model.generate(**inputs, **gen_kwargs)

    # Decode only the new tokens
    generated_ids = output_ids[0, inputs["input_ids"].shape[1]:]
    result = processor.decode(generated_ids, skip_special_tokens=True)
    return result.strip()


CSS = """
#col-container { max-width: 1100px; margin: 0 auto; }
.dark .gradio-container { color: var(--body-text-color); }
"""

with gr.Blocks() as demo:
    gr.Markdown(
        "# 🖥️ WebBrain VL 2 450M — Browser Screenshot Understanding\n"
        "Upload a browser screenshot to get a structured six-section observation "
        "for web-automation planning. Fine-tuned from "
        "[LFM2.5-VL-450M](https://huggingface.co/LiquidAI/LFM2.5-VL-450M)."
    )

    with gr.Column(elem_id="col-container"):
        with gr.Row():
            with gr.Column(scale=1):
                image_input = gr.Image(
                    label="Browser Screenshot",
                    type="pil",
                    height=400,
                )
                run_btn = gr.Button("Analyze Screenshot", variant="primary")

                with gr.Accordion("Advanced settings", open=False):
                    max_tokens = gr.Slider(
                        label="Max new tokens",
                        minimum=64,
                        maximum=2048,
                        value=800,
                        step=64,
                    )
                    temp = gr.Slider(
                        label="Temperature",
                        minimum=0.0,
                        maximum=1.0,
                        value=0.0,
                        step=0.05,
                    )

            with gr.Column(scale=1):
                output = gr.Textbox(
                    label="Structured Observation",
                    lines=24,
                    max_lines=50,
                )

        run_btn.click(
            fn=analyze_screenshot,
            inputs=[image_input, max_tokens, temp],
            outputs=output,
            api_name="analyze",
        )

        gr.Examples(
            examples=[
                ["example_signin.png", 800, 0.0],
                ["example_search.png", 800, 0.0],
                ["example_checkout.png", 800, 0.0],
            ],
            inputs=[image_input, max_tokens, temp],
            outputs=output,
            fn=analyze_screenshot,
            cache_examples=True,
            cache_mode="lazy",
        )


if __name__ == "__main__":
    demo.launch(mcp_server=True, theme=gr.themes.Citrus(), css=CSS)