Image-Text-to-Text
Transformers
Safetensors
interfaze_lite
feature-extraction
vlm
any-to-text
audio-text-to-text
multimodal
multilingual
mixture-of-architectures
ocr
document-understanding
speech-recognition
speaker-diarization
object-detection
gui-grounding
image-segmentation
translation
time-series-forecasting
guardrails
structured-output
agent
custom_code
Instructions to use suryatmodulus/interfaze-1-lite with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use suryatmodulus/interfaze-1-lite with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="suryatmodulus/interfaze-1-lite", trust_remote_code=True)# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("suryatmodulus/interfaze-1-lite", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use suryatmodulus/interfaze-1-lite with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "suryatmodulus/interfaze-1-lite" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "suryatmodulus/interfaze-1-lite", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker
docker model run hf.co/suryatmodulus/interfaze-1-lite
- SGLang
How to use suryatmodulus/interfaze-1-lite with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "suryatmodulus/interfaze-1-lite" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "suryatmodulus/interfaze-1-lite", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "suryatmodulus/interfaze-1-lite" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "suryatmodulus/interfaze-1-lite", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }' - Docker Model Runner
How to use suryatmodulus/interfaze-1-lite with Docker Model Runner:
docker model run hf.co/suryatmodulus/interfaze-1-lite
Download grounding.py from suryatmodulus/interfaze-1-lite: direct link, hf CLI and curl.
- Browser
- Download file 13.5 kB
-
https://huggingface.co/suryatmodulus/interfaze-1-lite/resolve/main/grounding.py
- Command line
-
hf download hf://suryatmodulus/interfaze-1-lite/grounding.py
-
curl -L -o grounding.py https://huggingface.co/suryatmodulus/interfaze-1-lite/resolve/main/grounding.py
13.5 kB
| """Locating things with the brain: the question it is asked, and how its answer is read. | |
| Shared by the service (vLLM) and the transformers model (in-process), so a box found by one | |
| is the box the other finds. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import re | |
| from typing import Any | |
| try: # the interfaze_lite package, and the HF repo as trust_remote_code imports it | |
| from .contracts import _bounds_from_dict, iou, to_bounds | |
| except ImportError: # the files run flat, from a checkout | |
| from contracts import _bounds_from_dict, iou, to_bounds # type: ignore | |
| # A photo's box list is a few hundred tokens; 8192 only gave a degenerate reply room to run (233 boxes). | |
| MAX_TOKENS = 1536 | |
| # "All buttons" lists every one at ~33 tokens each: 1536 stopped after 46, in the menu bar. interfaze | |
| # allows 8192 on this model; a looping reply is cut down, not returned. | |
| MAX_TOKENS_UI = 8192 | |
| # interfaze's GUI instruction word for word (object-detection-truss, dev_qwen38_gui_main.py), same model. | |
| # Asking for each element's visible text instead boxed every piece of text and missed the buy box. | |
| UI_INSTRUCTION = ( | |
| "Locate every {phrase} in this UI screenshot.\n" | |
| "Respond with ONLY a JSON array and nothing else. Each element must be " | |
| '{{"bbox_2d": [x1, y1, x2, y2], "label": "<short name of the element>"}}.\n' | |
| "Coordinates are normalized to a 0-1000 grid, where (0,0) is the top-left corner " | |
| "and (1000,1000) is the bottom-right corner, regardless of the image's pixel size.\n" | |
| "Return every distinct match, not just the best one. " | |
| "If the screenshot contains no such element, respond with exactly []." | |
| ) | |
| def max_tokens(domain: str) -> int: | |
| return MAX_TOKENS_UI if domain == "ui" else MAX_TOKENS | |
| _OBJECT = re.compile(r'\{[^{}]*?"bbox_2d"\s*:\s*\[[^\]]*\][^{}]*?\}', re.S) | |
| def prompt(prompts: list[str], domain: str = "ui") -> str: | |
| """The grounding question for `prompts` in a screenshot ("ui") or a photograph. | |
| The wording matters: asked for elements "in this UI screenshot", a photograph often returns []. | |
| """ | |
| phrase = ", ".join(prompts) | |
| if domain == "ui": | |
| return UI_INSTRUCTION.format(phrase=phrase) | |
| # Ranking took RefCOCOg 77.5% -> 81.2%, but ScreenSpot-v2 fell to 51.2% with empty | |
| # replies 1.2% -> 18.8%, so screenshots skip it. | |
| return ( | |
| f"Locate every {phrase} in this image.\n" | |
| "Label each match with the kind of object it is.\n" | |
| "Respond with ONLY a JSON array, each element " | |
| '{"bbox_2d": [x1,y1,x2,y2], "label": "<label>"}.\n' | |
| "Coordinates are normalized to a 0-1000 grid, (0,0) top-left, " | |
| "(1000,1000) bottom-right.\n" | |
| "Order the array so the element that best answers the request " | |
| "comes first; callers that want one answer take that one.\n" | |
| "Return every distinct match. If none, respond with exactly []." | |
| ) | |
| def _element(text: str, fallback_label: str) -> dict[str, Any] | None: | |
| try: | |
| obj = json.loads(text) | |
| bounds = to_bounds(obj["bbox_2d"], src="norm1000", img_w=1000, img_h=1000) | |
| except (json.JSONDecodeError, KeyError, ValueError, TypeError): | |
| return None | |
| if bounds.width <= 0 or bounds.height <= 0: | |
| return None | |
| return { | |
| "bounds": bounds.as_dict(), | |
| "label": obj.get("label") or fallback_label, | |
| "coordinate_space": "normalized_1000", | |
| } | |
| def parse(raw: str, fallback_label: str, *, looped: bool = False) -> list[dict[str, Any]]: | |
| """The boxes in a grounding reply, on the 0-1000 grid, repeats removed. | |
| The grid is resolution-independent: `to_pixels` places the boxes on the image. | |
| A reply that `looped` also loses every box it repeated: those are the loop. | |
| """ | |
| elements = [e for m in _OBJECT.finditer(raw or "") if (e := _element(m.group(0), fallback_label))] | |
| if looped: | |
| elements = _unrepeated(elements) | |
| return suppress_duplicates(cut_ladders(elements)) | |
| def _unrepeated(elements: list[dict[str, Any]], threshold: float = 0.85) -> list[dict[str, Any]]: | |
| """The boxes a looping reply gave once; what it went round and round is the loop, not a find.""" | |
| repeated: set[int] = set() | |
| for i in range(len(elements)): | |
| j = next((j for j in range(i) if _same_box(elements[i], elements[j], threshold)), None) | |
| if j is not None: | |
| repeated.update((i, j)) | |
| return [e for i, e in enumerate(elements) if i not in repeated] | |
| # Wasted boxes (repeats, or off the grid) before a reply counts as looping; healthy lists repeat now and then. | |
| LOOP_REPEATS = 10 | |
| class LoopWatch: | |
| """Reads a grounding reply as it streams and says when it has started looping. | |
| Stopping at LOOP_REPEATS gives back a minute of nothing (results.md, "GUI grounding looped"). | |
| """ | |
| def __init__(self, repeats: int = LOOP_REPEATS): | |
| self.repeats = repeats | |
| self.raw = "" | |
| self.wasted = 0 | |
| self._pos = 0 | |
| self._kept: list[dict[str, Any]] = [] | |
| def feed(self, text: str) -> bool: | |
| """Add the next piece of the reply; True once it is looping.""" | |
| self.raw += text | |
| for match in _OBJECT.finditer(self.raw, self._pos): | |
| self._pos = match.end() | |
| element = _element(match.group(0), "") | |
| if element is None or any(_same_box(element, k) for k in self._kept): | |
| self.wasted += 1 | |
| else: | |
| self._kept.append(element) | |
| return self.wasted >= self.repeats | |
| def _bounds_dict(x1: int, y1: int, x2: int, y2: int) -> dict[str, Any]: | |
| return { | |
| "top_left": {"x": x1, "y": y1}, | |
| "top_right": {"x": x2, "y": y1}, | |
| "bottom_right": {"x": x2, "y": y2}, | |
| "bottom_left": {"x": x1, "y": y2}, | |
| "width": x2 - x1, | |
| "height": y2 - y1, | |
| } | |
| def tiles(width: int, height: int, overlap: float = 0.1) -> list[tuple[int, int, int, int]]: | |
| """A screenshot as four overlapping quarters, (x1, y1, x2, y2) in its pixels. | |
| What a looping screenshot is grounded in instead: fewer elements, seen at higher resolution. | |
| The overlap keeps an element on a seam whole in one of them. | |
| """ | |
| half_w, half_h = width / 2, height / 2 | |
| pad_w, pad_h = width * overlap / 2, height * overlap / 2 | |
| xs = [(0, round(half_w + pad_w)), (round(half_w - pad_w), width)] | |
| ys = [(0, round(half_h + pad_h)), (round(half_h - pad_h), height)] | |
| return [(x1, y1, x2, y2) for y1, y2 in ys for x1, x2 in xs] | |
| def from_tile( | |
| elements: list[dict[str, Any]], tile: tuple[int, int, int, int], width: int, height: int, margin: int = 5 | |
| ) -> list[dict[str, Any]]: | |
| """Boxes on a tile's 0-1000 grid, moved onto the whole screenshot's grid. | |
| A box against a seam is dropped: the crop cut it in half, and the overlap shows it whole next door. | |
| """ | |
| x1, y1, x2, y2 = tile | |
| tw, th = x2 - x1, y2 - y1 | |
| def on_seam(b): | |
| return ( | |
| (x1 > 0 and b["top_left"]["x"] <= margin) | |
| or (x2 < width and b["bottom_right"]["x"] >= 1000 - margin) | |
| or (y1 > 0 and b["top_left"]["y"] <= margin) | |
| or (y2 < height and b["bottom_right"]["y"] >= 1000 - margin) | |
| ) | |
| def point(p): | |
| return { | |
| "x": round((x1 + p["x"] / 1000 * tw) / width * 1000), | |
| "y": round((y1 + p["y"] / 1000 * th) / height * 1000), | |
| } | |
| out = [] | |
| for element in elements: | |
| if on_seam(element["bounds"]): | |
| continue | |
| tl, br = point(element["bounds"]["top_left"]), point(element["bounds"]["bottom_right"]) | |
| out.append({**element, "bounds": _bounds_dict(tl["x"], tl["y"], br["x"], br["y"])}) | |
| return out | |
| def cut_ladders(elements: list[dict[str, Any]], run: int = 6) -> list[dict[str, Any]]: | |
| """Drop a stack of identical boxes each starting exactly where the last one ends; the first is kept. | |
| A looping reply's other shape: one box stepped across the page by its own size, any direction, | |
| with drift and a quarter (or three units) of jitter (results.md, "Ladders"). Real lists have gaps. | |
| """ | |
| def edges(e): | |
| b = e["bounds"] | |
| return (b["top_left"]["x"], b["top_left"]["y"], b["bottom_right"]["x"], b["bottom_right"]["y"]) | |
| def alike(u, v): | |
| return abs(u - v) <= max(3, 0.25 * max(u, v)) | |
| def stacked(a, b): | |
| ax1, ay1, ax2, ay2 = edges(a) | |
| bx1, by1, bx2, by2 = edges(b) | |
| w, h = ax2 - ax1, ay2 - ay1 | |
| if not (alike(w, bx2 - bx1) and alike(h, by2 - by1)): | |
| return False | |
| sideways = abs(bx1 - ax2) <= 1 or abs(bx2 - ax1) <= 1 | |
| upright = abs(by1 - ay2) <= 1 or abs(by2 - ay1) <= 1 | |
| return (sideways and abs(by1 - ay1) <= max(1, 0.15 * h)) or ( | |
| upright and abs(bx1 - ax1) <= max(1, 0.15 * w) | |
| ) | |
| drop: set[int] = set() | |
| start = 0 | |
| for i in range(1, len(elements) + 1): | |
| if i < len(elements) and stacked(elements[i - 1], elements[i]): | |
| continue | |
| if i - start >= run: | |
| drop.update(range(start + 1, i)) | |
| start = i | |
| return [e for i, e in enumerate(elements) if i not in drop] | |
| def suppress_duplicates(elements: list[dict[str, Any]], threshold: float = 0.85) -> list[dict[str, Any]]: | |
| """Drop boxes that repeat an earlier box of the same label, as detectors do; replies are best-first.""" | |
| kept: list[dict[str, Any]] = [] | |
| for element in elements: | |
| if not any(_same_box(element, k, threshold) for k in kept): | |
| kept.append(element) | |
| return kept | |
| def _same_box(a: dict[str, Any], b: dict[str, Any], threshold: float = 0.85) -> bool: | |
| """One element given twice: the same label on nearly the same box.""" | |
| return ( | |
| a["label"] == b["label"] | |
| and iou(_bounds_from_dict(a["bounds"]), _bounds_from_dict(b["bounds"])) >= threshold | |
| ) | |
| def trim_to_content( | |
| elements: list[dict[str, Any]], image, tolerance: int = 12, share: float = 1.0 | |
| ) -> list[dict[str, Any]]: | |
| """Screenshot boxes with their plain-background margins cut off, on the 0-1000 grid. | |
| Each side is trimmed against the colour just outside it: an edge row or column is margin while | |
| `share` of it is that colour. A box only shrinks; one with nothing in it is dropped (results.md, | |
| "GUI boxes too wide"). `share` is all of it: 0.9 trimmed through thin borders, 0.98 lost underlines. | |
| """ | |
| import numpy as np | |
| pixels = np.asarray(image.convert("RGB")).astype(np.int16) | |
| h, w = pixels.shape[:2] | |
| out = [] | |
| for element in elements: | |
| b = element["bounds"] | |
| x1, y1 = max(0, round(b["top_left"]["x"] / 1000 * w)), max(0, round(b["top_left"]["y"] / 1000 * h)) | |
| x2, y2 = ( | |
| min(w, round(b["bottom_right"]["x"] / 1000 * w)), | |
| min(h, round(b["bottom_right"]["y"] / 1000 * h)), | |
| ) | |
| if x2 - x1 <= 2 or y2 - y1 <= 2: | |
| out.append(element) | |
| continue | |
| t = _trim_box(pixels, x1, y1, x2, y2, tolerance, share) | |
| if t is None: | |
| continue | |
| if t == (x1, y1, x2, y2): | |
| out.append(element) # nothing to trim: returned as it came, not re-rounded | |
| continue | |
| tl = {"x": round(t[0] / w * 1000), "y": round(t[1] / h * 1000)} | |
| br = {"x": round(t[2] / w * 1000), "y": round(t[3] / h * 1000)} | |
| out.append({**element, "bounds": _bounds_dict(tl["x"], tl["y"], br["x"], br["y"])}) | |
| return out | |
| def _trim_box(pixels, x1: int, y1: int, x2: int, y2: int, tolerance: int, share: float, ring: int = 4): | |
| """The box [x1, x2) x [y1, y2) with its margins trimmed, or None if all of it is margin.""" | |
| import numpy as np | |
| h, w = pixels.shape[:2] | |
| def outside(band): | |
| # The colour of a strip beside the box, if one colour holds most of it. | |
| around = band.reshape(-1, 3) | |
| if not len(around): | |
| return None | |
| colour = np.median(around, axis=0) | |
| return colour if (np.abs(around - colour).max(axis=1) <= tolerance).mean() >= 0.6 else None | |
| def plain(strip, colour) -> bool: | |
| return colour is not None and (np.abs(strip - colour).max(axis=1) <= tolerance).mean() >= share | |
| left = outside(pixels[y1:y2, max(0, x1 - ring) : x1]) | |
| while x1 < x2 and plain(pixels[y1:y2, x1], left): | |
| x1 += 1 | |
| right = outside(pixels[y1:y2, x2 : min(w, x2 + ring)]) | |
| while x1 < x2 and plain(pixels[y1:y2, x2 - 1], right): | |
| x2 -= 1 | |
| top = outside(pixels[max(0, y1 - ring) : y1, x1:x2]) | |
| while y1 < y2 and plain(pixels[y1, x1:x2], top): | |
| y1 += 1 | |
| bottom = outside(pixels[y2 : min(h, y2 + ring), x1:x2]) | |
| while y1 < y2 and plain(pixels[y2 - 1, x1:x2], bottom): | |
| y2 -= 1 | |
| return (x1, y1, x2, y2) if x1 < x2 and y1 < y2 else None | |
| def to_pixels(elements: list[dict], width: int | None, height: int | None) -> list[dict]: | |
| """Move boxes from the 0-1000 grid onto a `width` x `height` image. | |
| Every caller gets pixels: read as pixels, grid boxes once scored gui_detection 3.8% on ScreenSpot-v2. | |
| """ | |
| if not width or not height: | |
| return elements | |
| out = [] | |
| for element in elements: | |
| top_left = element["bounds"]["top_left"] | |
| bottom_right = element["bounds"]["bottom_right"] | |
| x1, y1 = round(top_left["x"] / 1000 * width), round(top_left["y"] / 1000 * height) | |
| x2, y2 = round(bottom_right["x"] / 1000 * width), round(bottom_right["y"] / 1000 * height) | |
| out.append( | |
| {k: v for k, v in element.items() if k != "coordinate_space"} | |
| | {"bounds": _bounds_dict(x1, y1, x2, y2)} | |
| ) | |
| return out | |