File size: 4,745 Bytes
7d6d0a9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import spaces  # before torch, so ZeroGPU can manage CUDA

import gradio as gr
import torch
from peft import PeftModel
from PIL import Image
from transformers import AutoProcessor, AutoTokenizer, Qwen3_5ForConditionalGeneration

from jemm import DecisionModel
from jemm.model import _decision_config

BASE = "Qwen/Qwen3.8-27B"
ADAPTER = "MaestroYan/JEMM"
MAX_PIXELS = 1280 * 800

# Attach the adapter on CPU, then .to("cuda") once, so ZeroGPU can pack all weights at startup
base = Qwen3_5ForConditionalGeneration.from_pretrained(BASE, dtype=torch.bfloat16, attn_implementation="sdpa")
peft_model = PeftModel.from_pretrained(base, ADAPTER, is_trainable=False, torch_device="cpu").to("cuda")
model = DecisionModel(peft_model, AutoTokenizer.from_pretrained(BASE), name="JEMM",
                      processor=AutoProcessor.from_pretrained(BASE), **_decision_config(ADAPTER))


def fit(image: Image.Image) -> Image.Image:
    image = image.convert("RGB")
    scale = (MAX_PIXELS / (image.width * image.height)) ** 0.5
    if scale >= 1:
        return image
    return image.resize((round(image.width * scale), round(image.height * scale)), Image.Resampling.LANCZOS)


@spaces.GPU(size="xlarge", duration=20)
def decide(state: str, question: str, candidates: str, image: Image.Image | None = None):
    """Pick one candidate for the question and return a probability for every candidate.

    Args:
        state: Context the decision is based on, such as a conversation or a task description.
        question: The question to answer.
        candidates: 2 to 32 distinct candidates, one per line.
        image: Optional screenshot. Larger images are scaled to about 1280x800, the training size.
    """
    options = [line.strip() for line in candidates.splitlines() if line.strip()]
    if not 2 <= len(options) <= 32 or len(set(options)) != len(options):
        raise gr.Error("Enter 2 to 32 distinct candidates, one per line.")
    questions = {"q": {"type": "choice", "instructions": question, "criteria": {o: o for o in options}}}
    try:
        result = model.decide(state, questions, None if image is None else [fit(image)])["q"]
    except ValueError as exc:
        raise gr.Error(str(exc)) from exc
    verdict = "" if result["confidence"] >= model.threshold else " · below threshold, treat as undecided"
    return result["probabilities"], f"**{result['choice']}**  \nConfidence {result['confidence']:.3f}{verdict}"


TEXT_EXAMPLES = [
    ["User: Will it rain in Paris tomorrow?", "Which tool should handle the request?",
     "get_weather: weather forecast for a city\nsearch_flights: flight search\nNo tool applies"],
    ["Customer: I was charged twice for my March invoice. Please refund one of the charges.",
     "Which team should handle this ticket?", "Billing\nTechnical support\nSales\nAccount security"],
]
SCREENSHOT_EXAMPLES = [
    ["Task: Search for wireless headphones.\nPrevious actions: [textbox] Search -> TYPE: wireless headphones",
     "Which element should be clicked next? Candidates refer to the elements marked with the same letters on the screenshot.",
     "\n".join(f"CLICK the element marked {x}" for x in "ABCDE"), "examples/search.png"],
]

HEADER = """# JEMM
**Like [Jev](https://typesafe.ai/blog/introducing-system-one-models-and-jev), but multimodal and open-weight.** Picks one candidate per question and returns a probability for every candidate, from text or text plus a screenshot.
[Model card](https://huggingface.co/MaestroYan/JEMM) · [GitHub](https://github.com/ypcypc/JEMM) · Not affiliated with TypeSafe AI."""


def panel(name: str, examples: list, image: bool):
    with gr.Tab(name):
        with gr.Row(equal_height=False):
            with gr.Column():
                state = gr.Textbox(label="State", lines=4)
                question = gr.Textbox(label="Question")
                candidates = gr.Textbox(label="Candidates, one per line", lines=5)
                inputs = [state, question, candidates]
                if image:
                    inputs.append(gr.Image(label="Screenshot", type="pil"))
                button = gr.Button("Decide", variant="primary")
            with gr.Column():
                result = gr.Markdown()
                probabilities = gr.Label(label="Probabilities", show_label=True)
        button.click(decide, inputs, [probabilities, result], api_name="decide" if not image else "decide_screenshot")
        gr.Examples(examples, inputs, cache_examples=False)


with gr.Blocks(title="JEMM") as demo:
    gr.Markdown(HEADER)
    panel("Text", TEXT_EXAMPLES, image=False)
    panel("Screenshot", SCREENSHOT_EXAMPLES, image=True)

demo.launch(theme=gr.themes.Default(primary_hue="blue", font=[gr.themes.GoogleFont("Inter"), "sans-serif"]))