#!/usr/bin/env python3 """Gradio web demo for the released WeVisOne-VL checkpoint.""" from __future__ import annotations import argparse from dataclasses import dataclass, field import html import inspect import os import re import sys import threading import time from pathlib import Path from typing import Any, Dict, List, Optional, Sequence, Tuple DEMO_DIR = Path(__file__).resolve().parent PROJECT_ROOT = DEMO_DIR.parent if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) for _proxy_key in ("no_proxy", "NO_PROXY"): _existing_no_proxy = [p.strip() for p in os.environ.get(_proxy_key, "").split(",") if p.strip()] for _local_host in ("localhost", "127.0.0.1", "0.0.0.0"): if _local_host not in _existing_no_proxy: _existing_no_proxy.append(_local_host) os.environ[_proxy_key] = ",".join(_existing_no_proxy) import gradio as gr import numpy as np from PIL import Image, ImageDraw, ImageFont try: from wevisone_vl.engine import DEFAULT_MODEL_ID, InferenceResult from wevisone_vl.prompts import build_prompt as build_engine_prompt except ImportError: import importlib.util DEFAULT_MODEL_ID = "tencent/WeVisOne-VL-4B" @dataclass class InferenceResult: # type: ignore[no-redef] response: str prompt: str boxes: list[list[float]] = field(default_factory=list) masks: list[np.ndarray] = field(default_factory=list) latency_s: float = 0.0 _prompts_spec = importlib.util.spec_from_file_location( "wevisone_vl_prompts", PROJECT_ROOT / "wevisone_vl" / "prompts.py" ) assert _prompts_spec and _prompts_spec.loader _prompts_mod = importlib.util.module_from_spec(_prompts_spec) _prompts_spec.loader.exec_module(_prompts_mod) build_engine_prompt = _prompts_mod.build_prompt EXAMPLES_IMAGE_DIR = DEMO_DIR / "examples" / "images" # --------------------------------------------------------------------------- # Task taxonomy (aligned with the WeVisOne-VL paper) # Part 1: Object-level Perception (6 tasks) # Part 2: Pixel-level Perception (2 tasks: Segmentation & Matting) # --------------------------------------------------------------------------- OBJECT_TASKS = ("referring", "detection", "counting", "point", "ocr", "caption") PIXEL_TASKS = ("segmentation", "matting") ALL_TASKS = OBJECT_TASKS + PIXEL_TASKS TASK_LABELS: Dict[str, str] = { "referring": "Grounding", "detection": "Dense Detection", "counting": "Counting", "point": "Pointing", "ocr": "OCR", "caption": "Caption", "segmentation": "Segmentation", "matting": "Matting", } COUNTING_MODES = ["Box Counting", "Point Counting"] OCR_MODES = ["All Text", "Text Grounding"] CAPTION_MODES = [ "Detailed Caption (with Boxes)", "One-Sentence Caption (with Boxes)", "Free-form / VQA", ] CAPTION_PRESET_PROMPTS: Dict[str, str] = { "Detailed Caption (with Boxes)": "describe the image with bounding boxes in detail", "One-Sentence Caption (with Boxes)": "describe the image with bounding boxes in one sentence.", "Free-form / VQA": "What is happening in this photo?", } DEFAULT_SUBTASK: Dict[str, str] = { "referring": "", "detection": "", "counting": "Box Counting", "point": "", "ocr": "All Text", "caption": "Detailed Caption (with Boxes)", "segmentation": "", "matting": "", } DEFAULT_QUERIES: Dict[Tuple[str, str], str] = { ("referring", ""): "boys holding microphone", ("detection", ""): "sofa\npillow\ncoffee table\nbook\nlamp\nvase", ("counting", "Box Counting"): "white chess pieces", ("counting", "Point Counting"): "white chess pieces", ("point", ""): "dogs", ("ocr", "All Text"): "", ("ocr", "Text Grounding"): "COFFEE", ("caption", "Detailed Caption (with Boxes)"): CAPTION_PRESET_PROMPTS["Detailed Caption (with Boxes)"], ("caption", "One-Sentence Caption (with Boxes)"): CAPTION_PRESET_PROMPTS["One-Sentence Caption (with Boxes)"], ("caption", "Free-form / VQA"): CAPTION_PRESET_PROMPTS["Free-form / VQA"], ("segmentation", ""): "parasol with alternating rings of light green and dark green with flowers", ("matting", ""): "person", } SEG_DISPLAY_MODES = ["Overlay", "Binary Mask", "Cutout"] MATTING_DISPLAY_MODES = ["Green Screen", "Overlay", "Alpha Matte", "Cutout"] MATTING_GREEN_RGB = (0.0, 214.0, 90.0) GALLERY_EXAMPLES: List[Dict[str, str]] = [ { "title": "Grounding · Singers", "image": "boys.jpg", "task": "referring", "subtask": "", "query": "boys holding microphone", }, { "title": "Dense Detection · Living Room", "image": "livingroom.jpg", "task": "detection", "subtask": "", "query": "sofa\npillow\ncoffee table\nbook\nlamp\nvase", }, { "title": "Counting · Chess Pieces", "image": "chess.jpg", "task": "counting", "subtask": "Box Counting", "query": "white chess pieces", }, { "title": "Pointing · Dogs", "image": "dogs.jpg", "task": "point", "subtask": "", "query": "dogs", }, { "title": "OCR · Coffee Sign", "image": "coffee_sign.jpg", "task": "ocr", "subtask": "All Text", "query": "", }, { "title": "Caption · Grounded Description", "image": "livingroom.jpg", "task": "caption", "subtask": "Detailed Caption (with Boxes)", "query": "describe the image with bounding boxes in detail", }, { "title": "Segmentation · Parasol", "image": "parasol.jpg", "task": "segmentation", "subtask": "", "query": "parasol with alternating rings of light green and dark green with flowers", }, { "title": "Matting · Portrait", "image": "seabreeze.jpg", "task": "matting", "subtask": "", "query": "person", }, ] # --------------------------------------------------------------------------- # Styling (unified sans-serif font, minimal copy) # --------------------------------------------------------------------------- UI_FONT_STACK = [ "-apple-system", "BlinkMacSystemFont", "Segoe UI", "Roboto", "Helvetica Neue", "Arial", "Noto Sans", "sans-serif", ] UI_CSS = """ .gradio-container { max-width: 1320px !important; margin: 0 auto !important; padding: 8px 28px 40px !important; } .wv-hero { text-align: center; padding: 32px 16px 20px; } .wv-hero h1 { font-size: 2.2rem; font-weight: 800; letter-spacing: -0.02em; line-height: 1.2; margin: 0; } .wv-hero .wv-grad { background: linear-gradient(90deg, #6366f1, #8b5cf6 50%, #06b6d4); -webkit-background-clip: text; background-clip: text; color: transparent; } .wv-hero p { margin: 12px auto 0; max-width: 720px; font-size: 1.02rem; line-height: 1.6; color: var(--body-text-color-subdued); } .wv-links { display: flex; justify-content: center; flex-wrap: wrap; gap: 10px; margin-top: 18px; } .wv-links a { display: inline-flex; align-items: center; gap: 6px; padding: 6px 14px; border-radius: 999px; border: 1px solid var(--border-color-primary); background: var(--block-background-fill); color: var(--body-text-color) !important; font-size: 0.88rem; font-weight: 600; text-decoration: none !important; transition: border-color .15s, transform .15s; } .wv-links a:hover { border-color: var(--color-accent); transform: translateY(-1px); } .wv-card { background: #ffffff !important; border: 1px solid var(--border-color-primary) !important; border-radius: 20px !important; padding: 22px 24px !important; gap: 16px !important; box-shadow: 0 1px 2px rgba(15, 23, 42, .04), 0 10px 30px -12px rgba(15, 23, 42, .12); } .dark .wv-card { background: var(--neutral-900) !important; } .wv-card .block { box-shadow: none !important; } .wv-main { gap: 24px !important; align-items: stretch; } .wv-section { gap: 14px !important; margin-top: 24px; } .wv-title { margin: 0 !important; } .wv-title h3 { display: flex; align-items: center; gap: 10px; margin: 0 !important; font-size: 1.05rem !important; font-weight: 700 !important; } .wv-title h3 .wv-step { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 999px; font-size: .8rem; color: #fff; background: linear-gradient(135deg, #6366f1, #06b6d4); } .wv-task-bar { gap: 28px !important; flex-wrap: wrap !important; align-items: flex-start; } .wv-task-col { gap: 10px !important; flex: 0 1 auto !important; width: auto !important; min-width: 0 !important; } .wv-task-label p { margin: 0 !important; font-size: .76rem !important; font-weight: 700 !important; letter-spacing: .08em; text-transform: uppercase; color: var(--body-text-color-subdued); } .wv-chip-row { gap: 8px !important; flex-wrap: wrap !important; } button.wv-chip { flex: 0 0 auto !important; width: auto !important; min-width: max-content !important; white-space: nowrap !important; border-radius: 999px !important; padding: 8px 18px !important; font-size: .92rem !important; font-weight: 600 !important; box-shadow: none !important; transition: all .15s ease; } button.wv-chip.secondary { background: transparent !important; border: 1px solid var(--border-color-primary) !important; color: var(--body-text-color) !important; } button.wv-chip.secondary:hover { border-color: var(--color-accent) !important; color: var(--color-accent) !important; } button.wv-chip.primary { border: 1px solid transparent !important; background: linear-gradient(90deg, #6366f1, #8b5cf6) !important; color: #fff !important; box-shadow: 0 6px 16px -6px rgba(99, 102, 241, .6) !important; } button.wv-run { border-radius: 14px !important; font-size: 1.02rem !important; font-weight: 700 !important; letter-spacing: .02em; background: linear-gradient(90deg, #6366f1, #8b5cf6 55%, #06b6d4) !important; border: none !important; color: #fff !important; box-shadow: 0 10px 24px -10px rgba(99, 102, 241, .7) !important; } button.wv-run:hover { filter: brightness(1.06); } .wv-stats { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; } .wv-stat { display: inline-flex; align-items: baseline; gap: 6px; padding: 6px 14px; border-radius: 999px; background: var(--background-fill-secondary); font-size: .88rem; color: var(--body-text-color-subdued); } .wv-stat b { font-size: 1rem; color: var(--body-text-color); } .wv-stat.wv-stat-hero b { font-size: 1.15rem; color: var(--color-accent); } .wv-legend { display: inline-flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: 999px; border: 1px solid var(--border-color-primary); font-size: .85rem; } .wv-legend i { width: 10px; height: 10px; border-radius: 999px; display: inline-block; } .wv-legend span { color: var(--body-text-color-subdued); } .wv-placeholder { font-size: .9rem; color: var(--body-text-color-subdued); } .wv-example-gallery .thumbnail-item { aspect-ratio: 4 / 3 !important; border-radius: 14px !important; overflow: hidden; transition: transform .15s ease, box-shadow .15s ease; } .wv-example-gallery .thumbnail-item:hover { transform: translateY(-2px); box-shadow: 0 12px 24px -12px rgba(15, 23, 42, .35); } """ HERO_HTML = """

WeVisOne-VL

Unified Visual Perception from Objects to Pixels

One model for grounding, detection, counting, pointing, OCR, grounded captioning, segmentation and matting. Pick a task, upload an image, and run.

""" EMPTY_SUMMARY_HTML = '
Run the model to see results here.
' def _section_title(step: int, text: str) -> gr.Markdown: return gr.Markdown( f'### {step}{text}', elem_classes=["wv-title"] ) # --------------------------------------------------------------------------- # Prompt construction # --------------------------------------------------------------------------- def resolve_engine_task(task: str, subtask: str) -> str: task = (task or "referring").strip().lower() subtask = (subtask or "").strip() if task == "grounding": return "referring" if task in ("dense detection", "dense_detection"): return "detection" if task == "counting": return "count_point" if subtask == "Point Counting" else "count_box" if task == "ocr": return "ocr_grounding" if subtask == "Text Grounding" else "ocr" if task == "caption": return "freeform" return task def build_task_prompt(task: str, subtask: str, query: str) -> str: engine_task = resolve_engine_task(task, subtask) text_query = (query or "").strip() if task == "caption": if not text_query: text_query = CAPTION_PRESET_PROMPTS.get( subtask, CAPTION_PRESET_PROMPTS["Detailed Caption (with Boxes)"] ) return text_query if engine_task != "ocr" and not text_query: default_q = DEFAULT_QUERIES.get((task, subtask)) or DEFAULT_QUERIES.get((task, ""), "object") text_query = default_q return build_engine_prompt(engine_task, text_query) # --------------------------------------------------------------------------- # Prediction parsing & visualization # --------------------------------------------------------------------------- _REF_RE = re.compile( r"(.*?)\s*<(box|point|polygon)>(.*?)(?:||$)", re.DOTALL | re.IGNORECASE, ) _COUNT_RE = re.compile(r"Total number:\s*(\d+)", re.IGNORECASE) _PALETTE: List[Tuple[int, int, int]] = [ (255, 92, 124), (47, 212, 123), (64, 156, 255), (255, 184, 48), (176, 92, 255), (0, 204, 196), (255, 112, 67), (120, 200, 80), ] _BOX_COLOR_AUTO = "Auto by Category" _BOX_COLOR_CUSTOM = "Custom" _BOX_COLOR_PRESETS: Dict[str, Optional[Tuple[int, int, int]]] = { _BOX_COLOR_AUTO: None, "Pink": (255, 92, 124), "Green": (47, 212, 123), "Blue": (64, 156, 255), "Amber": (255, 184, 48), "Purple": (176, 92, 255), "Cyan": (0, 204, 196), "White": (255, 255, 255), _BOX_COLOR_CUSTOM: None, } def _color_for(text: str) -> Tuple[int, int, int]: return _PALETTE[abs(hash(text)) % len(_PALETTE)] def _parse_hex_color(value: Any, default: Tuple[int, int, int] = (255, 92, 124)) -> Tuple[int, int, int]: if isinstance(value, (tuple, list)) and len(value) >= 3: return (int(value[0]), int(value[1]), int(value[2])) s = str(value or "").strip().lstrip("#") if len(s) == 3: s = "".join(ch * 2 for ch in s) if len(s) >= 6: try: return (int(s[0:2], 16), int(s[2:4], 16), int(s[4:6], 16)) except ValueError: pass return default def _resolve_box_color(preset: str, custom_hex: str, category: str) -> Tuple[int, int, int]: if preset == _BOX_COLOR_CUSTOM: return _parse_hex_color(custom_hex) rgb = _BOX_COLOR_PRESETS.get(preset) if rgb is not None: return rgb return _color_for(category) def _load_font(size: int) -> ImageFont.ImageFont: size = max(8, int(size)) for path in ( "/usr/share/fonts/urw-base35/NimbusSans-Regular.otf", "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", ): try: return ImageFont.truetype(path, size) except OSError: continue try: return ImageFont.load_default(size=size) except TypeError: return ImageFont.load_default() def _display_scale(image: Image.Image, target_w: float = 800.0) -> float: longest = max(int(image.width), int(image.height), 1) return max(0.75, min(longest / target_w, 5.0)) def _contrast_ink(bg: Tuple[int, int, int]) -> Tuple[int, int, int]: y = 0.299 * bg[0] + 0.587 * bg[1] + 0.114 * bg[2] return (255, 255, 255) if y < 186 else (20, 24, 32) def _draw_tag( draw: ImageDraw.ImageDraw, text: str, x: int, y: int, color: Tuple[int, int, int], font: ImageFont.ImageFont, *, above: bool = True, ) -> None: text = str(text or "") if not text: return bbox = draw.textbbox((0, 0), text, font=font) tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1] pad_x = max(5, int(round(th * 0.5))) pad_y = max(3, int(round(th * 0.3))) box_h = th + pad_y * 2 box_w = tw + pad_x * 2 top = y - box_h if above and y > box_h + 1 else y draw.rounded_rectangle( [x, top, x + box_w, top + box_h], radius=max(3, box_h // 3), fill=color ) draw.text( (x + pad_x - bbox[0], top + pad_y - bbox[1]), text, fill=_contrast_ink(color), font=font, ) def _draw_point( draw: ImageDraw.ImageDraw, x: int, y: int, color: Tuple[int, int, int], tag: str, font: ImageFont.ImageFont, scale: float, draw_width: int, show_labels: bool, ) -> None: r = max(8, int(round((8 + max(draw_width, 1)) * scale))) ring = max(2, int(round(2.5 * scale))) draw.ellipse([x - r - ring, y - r - ring, x + r + ring, y + r + ring], fill="white", outline="white") draw.ellipse([x - r, y - r, x + r, y + r], fill=color, outline=color) inner = max(3, r // 3) draw.ellipse([x - inner, y - inner, x + inner, y + inner], fill="white", outline="white") if show_labels and tag: _draw_tag(draw, tag, x + r + int(4 * scale), y - r, color, font, above=True) def parse_predictions_with_masks( text: str, width: int, height: int, masks: Sequence[np.ndarray], ) -> Dict[str, List[Dict[str, Any]]]: clean = (text or "").split("<|im_end|>", 1)[0].split("<|endoftext|>", 1)[0] predictions: Dict[str, List[Dict[str, Any]]] = {} mask_idx = 0 for label, shape, body in _REF_RE.findall(clean): cat = (label or "object").strip() or "object" bucket = predictions.setdefault(cat, []) kind = shape.lower() step = 2 if kind == "point" else 4 for segment in (body or "").split(","): values = [int(v) for v in re.findall(r"<(\d{1,4})>", segment)] has_seg = bool(re.search(r"<(?:OBJ|SEG)>", segment or "", flags=re.IGNORECASE)) for offset in range(0, len(values) - step + 1, step): chunk = values[offset : offset + step] if step == 2: coords = [chunk[0] / 1000.0 * width, chunk[1] / 1000.0 * height] else: coords = [ chunk[0] / 1000.0 * width, chunk[1] / 1000.0 * height, chunk[2] / 1000.0 * width, chunk[3] / 1000.0 * height, ] if coords[2] <= coords[0] or coords[3] <= coords[1]: continue item: Dict[str, Any] = {"type": kind, "coords": coords} if has_seg and mask_idx < len(masks): item["mask"] = masks[mask_idx] mask_idx += 1 bucket.append(item) if not bucket: predictions.pop(cat, None) if mask_idx == 0 and masks: flat_boxes = [ ann for anns in predictions.values() for ann in anns if ann.get("type") == "box" ] for ann, m in zip(flat_boxes, masks): ann["mask"] = m return predictions def extract_count(text: str, predictions: Dict[str, List[Dict[str, Any]]], *, counting: bool = False) -> Optional[int]: m = _COUNT_RE.search(text or "") if m: return int(m.group(1)) n = sum(len(v) for v in predictions.values()) if n: return n if counting: nums = re.findall(r"-?\d+", text or "") if nums: return int(nums[-1]) return None def _resize_mask(mask: np.ndarray, width: int, height: int) -> np.ndarray: if mask.shape[0] == height and mask.shape[1] == width: return mask img = Image.fromarray( np.clip(mask * 255.0 if mask.dtype != np.uint8 else mask, 0, 255).astype(np.uint8), mode="L", ).resize((width, height), resample=Image.Resampling.BILINEAR) return np.asarray(img, dtype=np.float32) / 255.0 def _combined_alpha( predictions: Dict[str, List[Dict[str, Any]]], raw_masks: Sequence[np.ndarray], width: int, height: int, ) -> np.ndarray: combined = np.zeros((height, width), dtype=np.float32) found = False for anns in predictions.values(): for ann in anns: raw_mask = ann.get("mask") if raw_mask is None: continue mask = np.asarray(raw_mask, dtype=np.float32) if mask.max() > 1.0: mask = mask / 255.0 mask = np.clip(mask, 0.0, 1.0) combined = np.maximum(combined, _resize_mask(mask, width, height)) found = True if not found: for raw_mask in raw_masks: mask = np.clip(np.asarray(raw_mask, dtype=np.float32), 0.0, 1.0) combined = np.maximum(combined, _resize_mask(mask, width, height)) return combined def _overlay_masks( image: Image.Image, predictions: Dict[str, List[Dict[str, Any]]], raw_masks: Sequence[np.ndarray], *, box_color_preset: str, box_color_hex: str, mask_alpha: float, mask_threshold: float, mask_mode: str = "binary", ) -> Image.Image: arr = np.asarray(image.convert("RGB"), dtype=np.float32) height, width = arr.shape[:2] alpha = float(max(0.0, min(1.0, mask_alpha))) if alpha <= 0: return image.copy() painted = False for category, anns in predictions.items(): color = _resolve_box_color(box_color_preset, box_color_hex, category) for ann in anns: raw_mask = ann.get("mask") if raw_mask is None: continue mask = np.asarray(raw_mask, dtype=np.float32) if mask.max() > 1.0: mask = mask / 255.0 mask = np.clip(mask, 0.0, 1.0) mask = _resize_mask(mask, width, height) if mask_mode == "matting": keep = mask > 0.0 weight = (mask * alpha)[..., None] else: keep = mask >= float(mask_threshold) weight = (keep.astype(np.float32) * alpha)[..., None] if not np.any(keep): continue color_arr = np.array(color, dtype=np.float32).reshape(1, 1, 3) arr = np.where(keep[..., None], arr * (1.0 - weight) + color_arr * weight, arr) painted = True if not painted and raw_masks: combined = _combined_alpha(predictions, raw_masks, width, height) if mask_mode == "matting": keep = combined > 0.0 weight = (combined * alpha)[..., None] else: keep = combined >= float(mask_threshold) weight = (keep.astype(np.float32) * alpha)[..., None] if np.any(keep): color_arr = np.array(_PALETTE[0], dtype=np.float32).reshape(1, 1, 3) arr = np.where(keep[..., None], arr * (1.0 - weight) + color_arr * weight, arr) painted = True if not painted: return image.copy() return Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8)) def render_from_cache( cache: Optional[Dict[str, Any]], font_size: int = 16, draw_width: int = 3, show_labels: bool = True, box_color_preset: str = _BOX_COLOR_AUTO, box_color_hex: str = "#ff5c7c", show_boxes: bool = True, show_masks: bool = True, mask_alpha: float = 0.45, mask_threshold: float = 0.5, pixel_display: str = "Overlay", ) -> Optional[Image.Image]: if not cache: return None arr = cache.get("pil") if arr is None: return None pil = arr.convert("RGB") if isinstance(arr, Image.Image) else Image.fromarray(np.asarray(arr)).convert("RGB") preds: Dict[str, List[Dict[str, Any]]] = cache.get("preds") or {} raw_masks: List[np.ndarray] = cache.get("masks") or [] task: str = cache.get("task") or "detection" is_pixel_task = task in PIXEL_TASKS width, height = pil.size if is_pixel_task and (preds or raw_masks): alpha = _combined_alpha(preds, raw_masks, width, height) effective_alpha = alpha if task == "matting" else (alpha >= float(mask_threshold)).astype(np.float32) alpha_u8 = np.clip(effective_alpha * 255.0, 0, 255).astype(np.uint8) if pixel_display in ("Alpha Matte", "Binary Mask"): return Image.fromarray(alpha_u8, mode="L") if pixel_display == "Green Screen": fg = np.asarray(pil, dtype=np.float32) green = np.empty_like(fg) green[...] = MATTING_GREEN_RGB w3 = effective_alpha[..., None] comp = fg * w3 + green * (1.0 - w3) return Image.fromarray(np.clip(comp, 0, 255).astype(np.uint8), mode="RGB") if pixel_display == "Cutout": rgba = pil.convert("RGBA") rgba.putalpha(Image.fromarray(alpha_u8, mode="L")) return rgba vis = pil.copy() if is_pixel_task and show_masks and (preds or raw_masks): vis = _overlay_masks( vis, preds, raw_masks, box_color_preset=box_color_preset, box_color_hex=box_color_hex, mask_alpha=float(mask_alpha), mask_threshold=float(mask_threshold), mask_mode=("matting" if task == "matting" else "binary"), ) if not preds: return vis if show_boxes and not is_pixel_task: fill_layer = Image.new("RGBA", vis.size, (0, 0, 0, 0)) fill_draw = ImageDraw.Draw(fill_layer) for category, anns in preds.items(): color = _resolve_box_color(box_color_preset, box_color_hex, category) for ann in anns: coords = ann.get("coords") or [] if ann.get("type", "box") == "box" and len(coords) == 4: x0, y0, x1, y1 = [int(c) for c in coords] if x1 > x0 and y1 > y0: fill_draw.rectangle([x0, y0, x1, y1], fill=(*color, 38)) vis = Image.alpha_composite(vis.convert("RGBA"), fill_layer).convert("RGB") draw = ImageDraw.Draw(vis) scale = _display_scale(vis) font = _load_font(max(11, int(round(int(font_size) * scale)))) lw = max(1, int(round(max(int(draw_width), 1) * scale))) for category, anns in preds.items(): color = _resolve_box_color(box_color_preset, box_color_hex, category) tag = str(category or "").strip() or "object" for ann in anns: kind = ann.get("type", "box") coords = ann.get("coords") or [] if kind == "box" and len(coords) == 4: x0, y0, x1, y1 = [int(c) for c in coords] if x1 <= x0 or y1 <= y0: continue if show_boxes: draw.rectangle([x0, y0, x1, y1], outline=color, width=lw) if show_labels and show_boxes: _draw_tag(draw, tag, x0, y0, color, font) elif kind == "point" and len(coords) == 2: x, y = int(coords[0]), int(coords[1]) _draw_point(draw, x, y, color, tag, font, scale, int(draw_width), bool(show_labels)) return vis # --------------------------------------------------------------------------- # Mock engine for UI preview without loading GPU weights # --------------------------------------------------------------------------- class MockEngine: """Stand-in engine used with ``--mock`` so the UI can be tested without a GPU.""" model_name_or_path = f"{DEFAULT_MODEL_ID} (mock)" backend = "mock" def predict( self, image: Image.Image, *, task: str = "freeform", query: str | Sequence[str] = "", prompt: Optional[str] = None, decode_matting: bool = True, **generation_kwargs: Any, ) -> InferenceResult: del generation_kwargs time.sleep(0.25) w, h = image.size actual_prompt = (prompt or "").strip() or build_engine_prompt(task, query) first_label = ( (query.splitlines()[0].strip() if isinstance(query, str) and query.strip() else "object") if query else "object" ) if task in {"point", "count_point"}: resp = ( f"{first_label}<320><460>,<640><510>" + ("\nTotal number: 2" if task == "count_point" else "") ) return InferenceResult(response=resp, prompt=actual_prompt, latency_s=0.25) if task == "count_box": resp = ( f"{first_label}<180><260><420><740>,<540><280><810><760>\n" "Total number: 2" ) return InferenceResult(response=resp, prompt=actual_prompt, latency_s=0.25) if task in {"segmentation", "matting"} and decode_matting: yy, xx = np.mgrid[:h, :w] dist2 = ((xx - w * 0.5) / (w * 0.26)) ** 2 + ((yy - h * 0.52) / (h * 0.34)) ** 2 matte = np.clip(1.25 - dist2, 0.0, 1.0).astype(np.float32) resp = f"{first_label}<240><180><760><860>" return InferenceResult( response=resp, prompt=actual_prompt, boxes=[[0.24, 0.18, 0.76, 0.86]], masks=[matte], latency_s=0.25, ) if task == "freeform": resp = ( f"In this scene, main subject<160><180><840><860> " "is clearly visible in the foreground." ) return InferenceResult(response=resp, prompt=actual_prompt, latency_s=0.25) resp = f"{first_label}<180><200><780><840>" return InferenceResult(response=resp, prompt=actual_prompt, latency_s=0.25) # --------------------------------------------------------------------------- # Gradio UI helpers # --------------------------------------------------------------------------- def _to_pil(image: Any) -> Optional[Image.Image]: if image is None: return None if isinstance(image, Image.Image): return image.convert("RGB") if isinstance(image, np.ndarray): return Image.fromarray(image).convert("RGB") if isinstance(image, (str, Path)) and Path(image).is_file(): return Image.open(str(image)).convert("RGB") return None def subtask_ui_for(task: str, subtask: Optional[str] = None) -> Dict[str, Any]: if task == "counting": val = subtask if subtask in COUNTING_MODES else DEFAULT_SUBTASK["counting"] return gr.update(visible=True, choices=COUNTING_MODES, value=val, label="Mode") if task == "ocr": val = subtask if subtask in OCR_MODES else DEFAULT_SUBTASK["ocr"] return gr.update(visible=True, choices=OCR_MODES, value=val, label="Mode") if task == "caption": val = subtask if subtask in CAPTION_MODES else DEFAULT_SUBTASK["caption"] return gr.update(visible=True, choices=CAPTION_MODES, value=val, label="Preset") return gr.update(visible=False) def query_ui_for(task: str, subtask: str, value: Optional[str] = None) -> Dict[str, Any]: if value is None: value = DEFAULT_QUERIES.get((task, subtask), DEFAULT_QUERIES.get((task, ""), "")) if task == "referring": return gr.update( visible=True, value=value, lines=2, label="Grounding Expression", placeholder="boys holding microphone", ) if task == "detection": return gr.update( visible=True, value=value, lines=4, label="Categories (one per line)", placeholder="sofa\npillow\ncoffee table", ) if task == "counting": return gr.update( visible=True, value=value, lines=1, label="Target Category", placeholder="white chess pieces", ) if task == "point": return gr.update( visible=True, value=value, lines=2, label="Pointing Target", placeholder="dogs", ) if task == "ocr": if subtask == "Text Grounding": return gr.update( visible=True, value=value, lines=1, label="Text to Locate", placeholder="COFFEE", ) return gr.update(visible=False, value="") if task == "caption": return gr.update( visible=True, value=value, lines=2, label="Instruction / Question", placeholder="describe the image with bounding boxes in detail", ) if task in PIXEL_TASKS: return gr.update( visible=True, value=value, lines=2, label="Referring Expression", placeholder="parasol with alternating rings of light green and dark green with flowers", ) return gr.update(visible=True, value=value, lines=2, label="Query") def pixel_display_ui_for(task: str) -> Dict[str, Any]: """Only Pixel-level tasks (Segmentation & Matting) show overlay/matte display controls.""" if task == "segmentation": return gr.update( visible=True, choices=SEG_DISPLAY_MODES, value="Overlay", label="Display Mode", ) if task == "matting": return gr.update( visible=True, choices=MATTING_DISPLAY_MODES, value="Green Screen", label="Display Mode", ) return gr.update(visible=False) def task_button_updates(active_task: str) -> List[Dict[str, Any]]: return [ gr.update(variant="primary" if active_task == t else "secondary") for t in ALL_TASKS ] def _style_kwargs(target: str) -> Dict[str, Any]: theme = gr.themes.Soft( primary_hue="indigo", secondary_hue="cyan", neutral_hue="slate", radius_size=gr.themes.sizes.radius_lg, font=UI_FONT_STACK, ).set( body_background_fill="#f5f6fa", body_background_fill_dark="*neutral_950", block_background_fill="*neutral_50", block_background_fill_dark="*neutral_800", block_border_width="1px", block_border_color="*neutral_200", block_border_color_dark="*neutral_700", block_shadow="none", block_radius="14px", block_label_background_fill="transparent", block_label_background_fill_dark="transparent", block_label_border_width="0px", block_label_text_color="*neutral_500", block_label_text_weight="600", block_title_background_fill="transparent", block_title_background_fill_dark="transparent", block_title_text_color="*neutral_600", block_title_text_color_dark="*neutral_300", block_title_text_weight="600", block_title_padding="0", block_label_padding="4px 8px", input_background_fill="#ffffff", input_background_fill_dark="*neutral_800", input_border_color="*neutral_200", input_shadow="none", input_shadow_focus="0 0 0 3px rgba(99, 102, 241, .18)", input_border_color_focus="*primary_400", layout_gap="16px", ) blocks_takes_css = "css" in inspect.signature(gr.Blocks.__init__).parameters return {"css": UI_CSS, "theme": theme} if (target == "blocks") == blocks_takes_css else {} def build_demo(engine: Any) -> gr.Blocks: engine_lock = threading.Lock() initial_example = GALLERY_EXAMPLES[0] initial_task = initial_example["task"] initial_subtask = initial_example["subtask"] initial_query = initial_example["query"] initial_prompt = build_task_prompt(initial_task, initial_subtask, initial_query) initial_img_path = EXAMPLES_IMAGE_DIR / initial_example["image"] initial_image = Image.open(initial_img_path).convert("RGB") if initial_img_path.is_file() else None gallery_items: List[Tuple[str, str]] = [] for ex in GALLERY_EXAMPLES: p = EXAMPLES_IMAGE_DIR / ex["image"] if p.is_file(): gallery_items.append((str(p), ex["title"])) with gr.Blocks(title="WeVisOne-VL Demo", **_style_kwargs("blocks")) as demo: gr.HTML(HERO_HTML) raw_image = gr.State(initial_image) vis_cache = gr.State(None) task_state = gr.State(initial_task) with gr.Column(elem_classes=["wv-card"]): _section_title(1, "Choose a task") with gr.Row(elem_classes=["wv-task-bar"]): with gr.Column(elem_classes=["wv-task-col"]): gr.Markdown("Object-level Perception", elem_classes=["wv-task-label"]) with gr.Row(elem_classes=["wv-chip-row"]): chip = {"size": "md", "scale": 0, "min_width": 0, "elem_classes": ["wv-chip"]} btn_referring = gr.Button("Grounding", variant="primary", **chip) btn_detection = gr.Button("Dense Detection", variant="secondary", **chip) btn_counting = gr.Button("Counting", variant="secondary", **chip) btn_point = gr.Button("Pointing", variant="secondary", **chip) btn_ocr = gr.Button("OCR", variant="secondary", **chip) btn_caption = gr.Button("Caption", variant="secondary", **chip) with gr.Column(elem_classes=["wv-task-col"]): gr.Markdown("Pixel-level Perception", elem_classes=["wv-task-label"]) with gr.Row(elem_classes=["wv-chip-row"]): btn_segmentation = gr.Button("Segmentation", variant="secondary", **chip) btn_matting = gr.Button("Matting", variant="secondary", **chip) task_buttons = [ btn_referring, btn_detection, btn_counting, btn_point, btn_ocr, btn_caption, btn_segmentation, btn_matting, ] with gr.Row(equal_height=False, elem_classes=["wv-main", "wv-section"]): with gr.Column(scale=1, elem_classes=["wv-card"]): _section_title(2, "Image & query") input_image = gr.Image( value=initial_image, label="Input Image", show_label=False, type="pil", height=440, ) subtask = gr.Dropdown( choices=COUNTING_MODES, value=COUNTING_MODES[0], allow_custom_value=True, label="Mode", visible=False, ) query = gr.Textbox( label="Grounding Expression", value=initial_query, lines=2, placeholder="boys holding microphone", ) with gr.Accordion("Prompt sent to the model (editable)", open=False): prompt_preview = gr.Textbox( show_label=False, value=initial_prompt, interactive=True, lines=3, ) run_btn = gr.Button("Run", variant="primary", size="lg", elem_classes=["wv-run"]) with gr.Accordion("Generation Parameters", open=False): temperature = gr.Slider(0.0, 1.5, value=0.0, step=0.05, label="temperature") top_p = gr.Slider(0.0, 1.0, value=0.95, step=0.01, label="top_p") top_k = gr.Slider(1, 100, value=50, step=1, label="top_k") max_tokens = gr.Slider(64, 8192, value=4096, step=64, label="max_new_tokens") repetition_penalty = gr.Slider( 1.0, 1.5, value=1.05, step=0.01, label="repetition_penalty" ) with gr.Accordion("Visualization Options", open=False): with gr.Row(): font_size = gr.Slider(10, 40, value=16, step=1, label="Font Size") draw_width = gr.Slider(1, 10, value=3, step=1, label="Line Width") with gr.Row(): show_boxes = gr.Checkbox(value=True, label="Show Boxes") show_labels = gr.Checkbox(value=True, label="Show Labels") with gr.Row(): box_color_preset = gr.Dropdown( choices=list(_BOX_COLOR_PRESETS.keys()), value=_BOX_COLOR_AUTO, label="Color Palette", ) box_color_hex = gr.ColorPicker(value="#ff5c7c", label="Custom Color") # Pixel-only visualization controls (hidden for Object-level tasks) with gr.Group(visible=False) as pixel_viz_group: show_masks = gr.Checkbox(value=True, label="Show Mask Overlay") mask_alpha = gr.Slider( 0.05, 0.95, value=0.45, step=0.05, label="Overlay Opacity" ) mask_threshold = gr.Slider( 0.1, 0.9, value=0.5, step=0.05, label="Segmentation Threshold", ) with gr.Column(scale=1, elem_classes=["wv-card"]): _section_title(3, "Result") output_image = gr.Image( label="Result", show_label=False, height=440, interactive=False, ) # Only visible when task is Segmentation or Matting pixel_display = gr.Radio( choices=SEG_DISPLAY_MODES, value="Overlay", label="Display Mode", visible=False, ) summary_md = gr.HTML(EMPTY_SUMMARY_HTML) with gr.Accordion("Model Output", open=True): raw_out = gr.Textbox( show_label=False, lines=6, max_lines=16, interactive=False, ) with gr.Column(elem_classes=["wv-card", "wv-section"]): gr.Markdown("### Examples", elem_classes=["wv-title"]) gr.Markdown( "Click an example to load the image, task and query, then press **Run**.", elem_classes=["wv-placeholder"], ) example_gallery = gr.Gallery( value=gallery_items, label="Examples", show_label=False, columns=4, height="auto", object_fit="cover", allow_preview=False, elem_classes=["wv-example-gallery"], ) # ------------------------------------------------------------------- # Event callbacks # ------------------------------------------------------------------- def switch_task(target_task: str): sub = DEFAULT_SUBTASK.get(target_task, "") default_q = DEFAULT_QUERIES.get( (target_task, sub), DEFAULT_QUERIES.get((target_task, ""), "") ) prompt_text = build_task_prompt(target_task, sub, default_q) is_pixel = target_task in PIXEL_TASKS return ( target_task, subtask_ui_for(target_task, sub), query_ui_for(target_task, sub, value=default_q), prompt_text, pixel_display_ui_for(target_task), gr.update(visible=is_pixel), gr.update(visible=(target_task == "segmentation")), *task_button_updates(target_task), ) def on_subtask_change(task: str, sub: str, current_query: str): task = task or "referring" if task not in ("counting", "ocr", "caption"): return gr.update(), gr.update() sub = sub or "" default_q = DEFAULT_QUERIES.get((task, sub)) new_q = default_q if default_q is not None else current_query prompt_text = build_task_prompt(task, sub, new_q) return ( query_ui_for(task, sub, value=new_q), prompt_text, ) def on_query_change(task: str, sub: str, current_query: str): return build_task_prompt(task or "referring", sub or "", current_query or "") def on_image_change(img: Optional[Image.Image]): return _to_pil(img) def run_inference( image: Optional[Image.Image], cached_raw_image: Optional[Image.Image], task: str, sub: str, q: str, user_prompt: str, temp: float, tp: float, tk: int, max_tok: int, rep_pen: float, f_size: int, d_width: int, s_boxes: bool, s_labels: bool, c_preset: str, c_hex: str, s_masks: bool, m_alpha: float, m_thresh: float, p_display: str, ): pil = _to_pil(image) or _to_pil(cached_raw_image) if pil is None: return None, "Please upload or select an image first.", "", None task = (task or "referring").strip().lower() sub = (sub or "").strip() engine_task = resolve_engine_task(task, sub) is_pixel = task in PIXEL_TASKS actual_prompt = (user_prompt or "").strip() if not actual_prompt: actual_prompt = build_task_prompt(task, sub, q) try: with engine_lock: result: InferenceResult = engine.predict( pil, task=engine_task, query=q or "", prompt=actual_prompt, decode_matting=is_pixel, max_new_tokens=int(max_tok), temperature=float(temp), top_p=float(tp), top_k=int(tk), repetition_penalty=float(rep_pen), ) except Exception as exc: return ( None, f"Inference error: {exc}", f'
Error: {html.escape(str(exc))}
', None, ) preds = parse_predictions_with_masks( result.response, pil.width, pil.height, result.masks ) count = extract_count(result.response, preds, counting=(task == "counting")) cache = { "pil": np.array(pil.convert("RGB")), "preds": preds, "masks": result.masks, "task": task, } vis = ( render_from_cache( cache, font_size=int(f_size), draw_width=int(d_width), show_labels=bool(s_labels), box_color_preset=c_preset, box_color_hex=c_hex, show_boxes=bool(s_boxes), show_masks=bool(s_masks), mask_alpha=float(m_alpha), mask_threshold=float(m_thresh), pixel_display=p_display, ) or pil ) n_inst = sum(len(v) for v in preds.values()) n_mask = len(result.masks) model_tag = getattr(engine, "model_name_or_path", DEFAULT_MODEL_ID) header = ( f"[{model_tag} | {TASK_LABELS.get(task, task)}] " f"latency={result.latency_s:.2f}s " f"instances={n_inst}" + (f" mattes={n_mask}" if is_pixel else "") + f"\nPrompt: {result.prompt}\n" + ("-" * 56) + "\n" ) def stat(label: str, value: Any, hero: bool = False) -> str: cls = "wv-stat wv-stat-hero" if hero else "wv-stat" return f'{html.escape(label)} {html.escape(str(value))}' stats: List[str] = [] if task == "counting": stats.append(stat("Count", count if count is not None else "N/A", hero=True)) if is_pixel: stats.append(stat("Masks", n_mask, hero=True)) if n_inst: stats.append(stat("Instances", n_inst, hero=not (is_pixel or task == "counting"))) stats.append(stat("Latency", f"{result.latency_s:.2f}s")) for cat, anns in preds.items(): r, g, b = _resolve_box_color(c_preset, c_hex, cat) stats.append( f'' f"{html.escape(cat)} ×{len(anns)}" ) summary = f'
{"".join(stats)}
' return vis, header + result.response, summary, cache def redraw_from_cache( cache: Optional[Dict[str, Any]], f_size: int, d_width: int, s_boxes: bool, s_labels: bool, c_preset: str, c_hex: str, s_masks: bool, m_alpha: float, m_thresh: float, p_display: str, ): vis = render_from_cache( cache, font_size=int(f_size), draw_width=int(d_width), show_labels=bool(s_labels), box_color_preset=c_preset, box_color_hex=c_hex, show_boxes=bool(s_boxes), show_masks=bool(s_masks), mask_alpha=float(m_alpha), mask_threshold=float(m_thresh), pixel_display=p_display, ) if vis is None: return gr.update() return vis def on_select_example(evt: gr.SelectData): idx = evt.index if isinstance(idx, (list, tuple)): idx = idx[0] if idx is None or idx >= len(GALLERY_EXAMPLES): return [gr.update() for _ in range(9 + len(ALL_TASKS))] ex = GALLERY_EXAMPLES[idx] task = ex["task"] sub = ex["subtask"] q = ex["query"] img_path = EXAMPLES_IMAGE_DIR / ex["image"] img = Image.open(img_path).convert("RGB") if img_path.is_file() else None prompt_text = build_task_prompt(task, sub, q) is_pixel = task in PIXEL_TASKS return ( img, img, task, subtask_ui_for(task, sub), query_ui_for(task, sub, value=q), prompt_text, pixel_display_ui_for(task), gr.update(visible=is_pixel), gr.update(visible=(task == "segmentation")), *task_button_updates(task), ) # Wire task buttons task_switch_outputs = [ task_state, subtask, query, prompt_preview, pixel_display, pixel_viz_group, mask_threshold, *task_buttons, ] for _task_name, _btn in zip(ALL_TASKS, task_buttons): _btn.click( fn=lambda t=_task_name: switch_task(t), inputs=[], outputs=task_switch_outputs, ) # Use .input (user-only) instead of .change so programmatic task switches # never overwrite prompt_preview with stale task_state. subtask.input( fn=on_subtask_change, inputs=[task_state, subtask, query], outputs=[query, prompt_preview], ) query.input( fn=on_query_change, inputs=[task_state, subtask, query], outputs=prompt_preview, ) input_image.change( fn=on_image_change, inputs=input_image, outputs=raw_image, ) run_btn.click( fn=run_inference, inputs=[ input_image, raw_image, task_state, subtask, query, prompt_preview, temperature, top_p, top_k, max_tokens, repetition_penalty, font_size, draw_width, show_boxes, show_labels, box_color_preset, box_color_hex, show_masks, mask_alpha, mask_threshold, pixel_display, ], outputs=[output_image, raw_out, summary_md, vis_cache], ) viz_controls = [ font_size, draw_width, show_boxes, show_labels, box_color_preset, box_color_hex, show_masks, mask_alpha, mask_threshold, pixel_display, ] for _ctrl in viz_controls: _ctrl.change( fn=redraw_from_cache, inputs=[vis_cache, *viz_controls], outputs=output_image, ) example_gallery.select( fn=on_select_example, inputs=[], outputs=[ input_image, raw_image, task_state, subtask, query, prompt_preview, pixel_display, pixel_viz_group, mask_threshold, *task_buttons, ], ) return demo def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="WeVisOne-VL Gradio Demo") parser.add_argument( "--model", "--model_path", dest="model", default=os.environ.get("WEVISONE_MODEL", DEFAULT_MODEL_ID), ) parser.add_argument("--backend", default="transformers", choices=("transformers", "vllm")) parser.add_argument("--device", default="auto") parser.add_argument("--server-name", "--server_name", dest="server_name", default="0.0.0.0") parser.add_argument( "--server-port", "--server_port", dest="server_port", type=int, default=int(os.environ.get("PORT", "8080")), ) parser.add_argument("--share", action="store_true") parser.add_argument("--mock", action="store_true", help="skip model loading; return mock outputs for UI preview") return parser.parse_args() def main() -> None: args = parse_args() if args.mock: engine: Any = MockEngine() else: from wevisone_vl import WeVisOneVL engine = WeVisOneVL(args.model, backend=args.backend, device=args.device) demo = build_demo(engine) demo.queue() demo.launch( server_name=args.server_name, server_port=args.server_port, share=args.share, allowed_paths=[str(EXAMPLES_IMAGE_DIR)], **_style_kwargs("launch"), ) if __name__ == "__main__": main()