Spaces:
Running on Zero
Running on Zero
| """app.py — HuggingFace ZeroGPU Space: the deterministic 12-task vision | |
| extraction + fusion pipeline as an interactive showcase. | |
| Stick an image in → get the full JSON readout (12 task JSONs + FusedScene + | |
| deterministic fused prompt + overlays). Every possibility in the system is a | |
| selectable toggle. | |
| ZeroGPU teardown-friendly design | |
| -------------------------------- | |
| * PYTORCH_CUDA_ALLOC_CONF is set BEFORE torch imports (OOM-probing batched path). | |
| * The always-on specialist models load ONCE at module level (CUDA-emulation | |
| outside `@spaces.GPU`; real CUDA inside) — the efficient, fork-friendly residency. | |
| * Optional structurer (0.8B / 9B) + age gate load on demand, single-resident. | |
| * GPU functions return only picklable CPU data (task/digest dicts + rendered PIL | |
| overlays). fuse()/fused_prompt()/build_semantic_association() run on the CPU in | |
| the main process — no GPU is held during fusion. | |
| The pipeline modules themselves are the real `qwen_test_runner` package, vendored | |
| verbatim by ../build_space.py. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| # ── ZeroGPU rule: set the allocator conf BEFORE torch is imported ──────────── | |
| os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") | |
| import json | |
| import tempfile | |
| import time | |
| import numpy as np | |
| from PIL import Image, ImageDraw | |
| import gradio as gr | |
| # spaces (ZeroGPU). Degrade to a no-op decorator when running off-platform. | |
| try: | |
| import spaces | |
| _HAS_SPACES = True | |
| except Exception: # pragma: no cover - local/CPU dev | |
| _HAS_SPACES = False | |
| class _NoSpaces: | |
| def GPU(*_a, **_k): | |
| def deco(fn): | |
| return fn | |
| return deco | |
| spaces = _NoSpaces() # type: ignore | |
| import torch | |
| # ── real pipeline (vendored package) ───────────────────────────────────────── | |
| import qwen_test_runner.vision.specialists_gpu as g | |
| from qwen_test_runner.vision.specialists import Solids | |
| from qwen_test_runner.vision import derive | |
| from qwen_test_runner.vision.fuse import ( | |
| solids_digest, | |
| fuse, | |
| phrases_for_grounding, | |
| build_semantic_association, | |
| ) | |
| from qwen_test_runner.vision.fuse_prompt import fused_prompt | |
| from qwen_test_runner.vision.tasks_vision import get_task, model_for | |
| from qwen_test_runner.vision.coords import CoordSpace | |
| from qwen_test_runner.model_runner import SYSTEM_PROMPT_JSON | |
| from qwen_test_runner.evaluator import parse_safely | |
| DEVICE = "cuda" if torch.cuda.is_available() else "cpu" | |
| IS_GPU_ENV = bool(os.environ.get("SPACES_ZERO_GPU")) or DEVICE == "cuda" | |
| MAX_DIM = 1024 # match production DECODE_MAX_DIM | |
| BATCH_CAP = 24 # interactive batch ceiling (ZeroGPU quota) | |
| # 12 deterministic tasks (11 from _build_tasks + semantic_association from fusion) | |
| DET_TASKS = [ | |
| "image_classification", "bbox_grounding", "ocr_text", | |
| "data_type_differentiation", "data_type_utilization", | |
| "structural_spatial_awareness", "depth_analysis", "subject_fixation", | |
| "segmentation", "outline_association", "style_structural_awareness", | |
| "semantic_association", | |
| ] | |
| # registry entries with no deterministic builder (shown, disabled) | |
| VLM_TASKS = ["vit_accuracy_to_prompt", "geometric_3d_object_id", "camera_rotational_offset"] | |
| VOCABS = {"COCO-80": g.COCO_CLASSES, "shapes": g.SHAPE_CLASSES} | |
| STRUCTURERS = {"off": None, "Qwen3.5-0.8B": "Qwen/Qwen3.5-0.8B", "Qwen3.5-9B": "Qwen/Qwen3.5-9B"} | |
| COORD_SPACES = ["norm_0_1000", "norm_0_1", "pixel_abs"] | |
| _PALETTE = [ | |
| (239, 71, 111), (17, 138, 178), (6, 214, 160), (255, 209, 102), | |
| (155, 93, 229), (241, 91, 181), (0, 187, 249), (254, 127, 45), | |
| ] | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| # Model residency (teardown-friendly) | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| _PIPE = None | |
| _OCR = None | |
| _AGE = None | |
| _STRUCT: dict = {} | |
| def get_pipe(): | |
| """The always-on specialist pipeline (GroundingDINO/SAM/Depth/SigLIP2[/OCR]).""" | |
| global _PIPE | |
| if _PIPE is None: | |
| with_ocr = os.environ.get("SPACE_WITH_OCR", "1") == "1" | |
| _PIPE = g.SpecialistPipeline(device=DEVICE, with_ocr=with_ocr) | |
| return _PIPE | |
| def _get_ocr(pipe): | |
| """OCR reader — from the pipeline if it loaded there, else a lazy singleton | |
| (the teardown-safe fallback when EasyOCR misbehaves at module level).""" | |
| global _OCR | |
| if pipe.ocr is not None: | |
| return pipe.ocr | |
| if _OCR is None: | |
| _OCR = g.load_ocr(DEVICE) | |
| return _OCR | |
| def _get_age_filter(): | |
| """Age-gate pre-filter — imported lazily (the module loads its model at import).""" | |
| global _AGE | |
| if _AGE is None: | |
| import importlib | |
| faf = importlib.import_module("face_age_filter") | |
| _AGE = faf.FaceAgeFilter(decision_mode="strict", batch_size=32) | |
| return _AGE | |
| class _Structurer: | |
| """Caption→struct (slot-registry JSON), mirroring the production ModelPack.""" | |
| def __init__(self, model_id: str): | |
| from transformers import AutoProcessor | |
| try: | |
| from transformers import AutoModelForMultimodalLM as _M | |
| except ImportError: # pragma: no cover | |
| from transformers import AutoModelForImageTextToText as _M | |
| self.proc = AutoProcessor.from_pretrained(model_id) | |
| tok = getattr(self.proc, "tokenizer", self.proc) | |
| tok.padding_side = "left" | |
| if tok.pad_token_id is None: | |
| tok.pad_token = tok.eos_token | |
| self.pad_id = tok.pad_token_id | |
| if DEVICE == "cuda": | |
| dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16 | |
| self.model = _M.from_pretrained(model_id, dtype=dtype, device_map="cuda").eval() | |
| else: | |
| self.model = _M.from_pretrained(model_id).to("cpu").eval() | |
| def structure(self, captions: list, max_tok: int = 512) -> list: | |
| msgs = [[{"role": "system", "content": SYSTEM_PROMPT_JSON}, | |
| {"role": "user", "content": c}] for c in captions] | |
| enc = self.proc.apply_chat_template( | |
| msgs, add_generation_prompt=True, tokenize=True, return_dict=True, | |
| return_tensors="pt", padding=True, enable_thinking=False).to(self.model.device) | |
| n_in = enc["input_ids"].shape[1] | |
| gen = self.model.generate(**enc, max_new_tokens=max_tok, do_sample=False, | |
| pad_token_id=self.pad_id) | |
| outs = [self.proc.decode(s, skip_special_tokens=True).strip() for s in gen[:, n_in:]] | |
| structs = [] | |
| for raw in outs: | |
| pr = parse_safely(raw) | |
| if pr.schema_valid and pr.parsed is not None: | |
| m = pr.parsed | |
| structs.append(m.model_dump() if hasattr(m, "model_dump") else m.dict()) | |
| else: | |
| structs.append(None) | |
| return structs | |
| def _get_structurer(model_id: str): | |
| if model_id in _STRUCT: | |
| return _STRUCT[model_id] | |
| _STRUCT.clear() # single-resident (evict on switch) | |
| if DEVICE == "cuda": | |
| torch.cuda.empty_cache() | |
| _STRUCT[model_id] = _Structurer(model_id) | |
| return _STRUCT[model_id] | |
| # Preload the always-on specialists at module level on a GPU/ZeroGPU env | |
| # (lazy on a CPU dev box so the module imports cheaply for tests). | |
| if IS_GPU_ENV: | |
| try: | |
| get_pipe() | |
| except Exception as e: # pragma: no cover | |
| print(f"[app] specialist preload deferred: {type(e).__name__}: {e}") | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| # Solidify orchestration (public batched primitives + threshold / skip control) | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| def _resolve_vocab(vocab_choice: str, custom: str) -> list: | |
| if vocab_choice == "custom": | |
| toks = [t.strip() for t in (custom or "").split(",") if t.strip()] | |
| return toks or g.COCO_CLASSES | |
| return VOCABS.get(vocab_choice, g.COCO_CLASSES) | |
| def _solidify(pipe, images, vocab, phrases_list, box_thr, text_thr, | |
| use_ocr, use_masks, use_depth, batch, gdino_batch) -> list: | |
| """Mirror SpecialistPipeline.solidify_batch, but pass detection thresholds and | |
| honour the specialist on/off toggles.""" | |
| images = list(images) | |
| solids = [] | |
| ocr_reader = _get_ocr(pipe) if use_ocr else None | |
| for start in range(0, len(images), batch): | |
| chunk = images[start:start + batch] | |
| p_chunk = phrases_list[start:start + batch] if phrases_list is not None else None | |
| boxes_list = [] | |
| for s2 in range(0, len(chunk), gdino_batch): | |
| boxes_list.extend(g.detect_batch( | |
| pipe.gdino, chunk[s2:s2 + gdino_batch], vocab, | |
| box_threshold=box_thr, text_threshold=text_thr, device=DEVICE)) | |
| if use_masks and pipe.sam is not None: | |
| boxes_list = g.segment_batch(pipe.sam, chunk, boxes_list, device=DEVICE) | |
| depths = (g.depth_map_batch(pipe.depth, chunk) | |
| if (use_depth and pipe.depth is not None) else [None] * len(chunk)) | |
| classes = (g.zero_shot_batch(pipe.siglip, chunk, vocab, device=DEVICE) | |
| if pipe.siglip is not None else [None] * len(chunk)) | |
| styles = (g.zero_shot_batch(pipe.siglip, chunk, g.STYLE_LABELS, device=DEVICE) | |
| if pipe.siglip is not None else [None] * len(chunk)) | |
| if p_chunk is not None and any(p_chunk): | |
| attrs = [] | |
| for s2 in range(0, len(chunk), gdino_batch): | |
| attrs.extend(g.ground_phrases_batch( | |
| pipe.gdino, chunk[s2:s2 + gdino_batch], | |
| p_chunk[s2:s2 + gdino_batch], device=DEVICE)) | |
| else: | |
| attrs = [[] for _ in chunk] | |
| for k, im in enumerate(chunk): | |
| s = Solids(size=im.size) | |
| s.boxes = boxes_list[k] | |
| s.depth = depths[k] | |
| s.gray = np.asarray(im.convert("L"), dtype=np.float32) | |
| if classes[k] is not None: | |
| s.class_top = classes[k][:5] | |
| s.style = styles[k][0]["label"] | |
| if ocr_reader is not None: | |
| s.ocr = g.ocr_read(ocr_reader, im) | |
| s.attr_boxes = attrs[k] | |
| solids.append(s) | |
| return solids | |
| def _solidify_oom(pipe, images, vocab, phrases_list, box_thr, text_thr, | |
| use_ocr, use_masks, use_depth, batch, gdino_batch) -> list: | |
| """OOM-halving wrapper (mirrors produce_fused_dataset's guard).""" | |
| solids, i, bs = [], 0, batch | |
| while i < len(images): | |
| chunk = images[i:i + bs] | |
| p_chunk = phrases_list[i:i + bs] if phrases_list is not None else None | |
| try: | |
| solids.extend(_solidify(pipe, chunk, vocab, p_chunk, box_thr, text_thr, | |
| use_ocr, use_masks, use_depth, bs, gdino_batch)) | |
| i += len(chunk) | |
| bs = batch | |
| except torch.cuda.OutOfMemoryError: # pragma: no cover | |
| torch.cuda.empty_cache() | |
| if bs == 1: | |
| solids.append(Solids(size=images[i].size)) | |
| i += 1 | |
| bs = batch | |
| else: | |
| bs = max(1, bs // 2) | |
| return solids | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| # GPU stage (everything that touches CUDA) — teardown-friendly | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| def _gpu_duration(images, *_a, **_k): | |
| n = len(images) if images else 1 | |
| return int(min(240, 25 + 9 * n)) | |
| def gpu_extract(images, vocab, box_thr, text_thr, use_ocr, use_masks, use_depth, | |
| structurer_id, captions_list, use_age, batch, gdino_batch, render): | |
| """All CUDA work in one allocation. Returns picklable CPU data: | |
| per-image (tasks, digest, overlays) + caption structs + age audits + timing.""" | |
| pipe = get_pipe() | |
| timing = {} | |
| n = len(images) | |
| audits = None | |
| if use_age: | |
| t = time.perf_counter() | |
| audits = [r.to_audit() for r in _get_age_filter().check_batch(images)] | |
| timing["age_s"] = round(time.perf_counter() - t, 3) | |
| structs_rows = [{} for _ in images] | |
| raws_rows = [{} for _ in images] | |
| if structurer_id and any(captions_list or []): | |
| t = time.perf_counter() | |
| st = _get_structurer(structurer_id) | |
| for idx, caps in enumerate(captions_list or []): | |
| caps = [c for c in (caps or []) if c and str(c).strip()] | |
| if not caps: | |
| continue | |
| got = st.structure(caps) | |
| structs_rows[idx] = {f"cap_{j}": s for j, s in enumerate(got)} | |
| raws_rows[idx] = {f"cap_{j}": c for j, c in enumerate(caps)} | |
| timing["struct_s"] = round(time.perf_counter() - t, 3) | |
| phrases_list = [phrases_for_grounding(sr) for sr in structs_rows] | |
| t = time.perf_counter() | |
| solids = _solidify_oom(pipe, images, vocab, phrases_list, box_thr, text_thr, | |
| use_ocr, use_masks, use_depth, batch, gdino_batch) | |
| timing["extract_s"] = round(time.perf_counter() - t, 3) | |
| results = [] | |
| for s, im in zip(solids, images): | |
| tasks = g.SpecialistPipeline._build_tasks(s) # CPU, torch-free, fast | |
| digest = solids_digest(s) | |
| overlays = _render_overlays(im, s) if render else None | |
| results.append({"tasks": tasks, "digest": digest, "overlays": overlays}) | |
| if DEVICE == "cuda": | |
| torch.cuda.empty_cache() | |
| timing["n_images"] = n | |
| return results, structs_rows, raws_rows, audits, timing | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| # CPU fusion + assembly (no GPU held) | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| def _task_valid(task: str, pred) -> bool: | |
| try: | |
| m = model_for(get_task(task)) | |
| m.model_validate(pred) if hasattr(m, "model_validate") else m(**pred) | |
| return True | |
| except Exception: | |
| return False | |
| def _assemble(results, structs_rows, raws_rows, audits, sizes, | |
| t_own, t_margin, dedup_iou, coord_space, task_filter): | |
| """Fuse each image's digest + structs → scene + prompt + one output row.""" | |
| rows = [] | |
| cs = CoordSpace(coord_space) | |
| for i, r in enumerate(results): | |
| tasks = dict(r["tasks"]) | |
| try: | |
| scene = fuse(r["digest"], structs_rows[i] or {}, raws_rows[i] or {}, | |
| t_own=t_own, t_margin=t_margin, dedup_iou=dedup_iou, coord_space=cs) | |
| tasks["semantic_association"] = build_semantic_association(scene) | |
| prompt = fused_prompt(scene) | |
| conf = float(scene["quality"]["overall_confidence"]) | |
| except Exception as e: # pragma: no cover | |
| scene, prompt, conf = {"__error__": f"{type(e).__name__}: {e}"}, "", 0.0 | |
| valid = {t: _task_valid(t, p) for t, p in tasks.items() if t != "__error__"} | |
| shown = {t: tasks[t] for t in tasks if (not task_filter or t in task_filter)} | |
| W, H = sizes[i] | |
| rows.append({ | |
| "tasks_json": shown, "tasks_valid": valid, "fused_json": scene, | |
| "prompt_fused": prompt, "fusion_confidence": round(conf, 4), | |
| "struct": structs_rows[i] or {}, "age_audit": (audits[i] if audits else None), | |
| "proc_width": W, "proc_height": H, | |
| "overlays": r.get("overlays"), | |
| }) | |
| return rows | |
| def _download_row(row: dict) -> str: | |
| payload = { | |
| "tasks_json": json.dumps(row["tasks_json"]), | |
| "tasks_valid": json.dumps(row["tasks_valid"]), | |
| "fused_json": json.dumps(row["fused_json"]), | |
| "prompt_fused": row["prompt_fused"], | |
| "fusion_confidence": row["fusion_confidence"], | |
| "struct": json.dumps(row["struct"]), | |
| "age_audit": json.dumps(row["age_audit"]), | |
| "proc_width": row["proc_width"], "proc_height": row["proc_height"], | |
| } | |
| f = tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") | |
| json.dump(payload, f, indent=2) | |
| f.close() | |
| return f.name | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| # Overlay rendering (from Solids, pixel space) | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| def _colorize_depth(depth: np.ndarray) -> Image.Image: | |
| d = np.asarray(depth, dtype=np.float32) | |
| lo, hi = float(d.min()), float(d.max()) | |
| n = (d - lo) / (hi - lo + 1e-6) # 0=far, 1=near | |
| # 3-stop gradient far(indigo)→mid(teal)→near(amber) | |
| stops = np.array([[40, 30, 90], [17, 138, 178], [255, 209, 102]], dtype=np.float32) | |
| x = n * 2.0 | |
| lo_i = np.clip(np.floor(x).astype(int), 0, 1) | |
| frac = (x - lo_i)[..., None] | |
| rgb = (stops[lo_i] * (1 - frac) + stops[lo_i + 1] * frac).astype(np.uint8) | |
| return Image.fromarray(rgb, "RGB") | |
| def _render_overlays(image: Image.Image, s: Solids) -> dict: | |
| base = image.convert("RGB") | |
| annotated = base.copy() | |
| overlay = Image.new("RGBA", annotated.size, (0, 0, 0, 0)) | |
| od = ImageDraw.Draw(overlay) | |
| dr = ImageDraw.Draw(annotated) | |
| for i, b in enumerate(s.boxes): | |
| color = _PALETTE[i % len(_PALETTE)] | |
| x1, y1, x2, y2 = [int(v) for v in b["box"]] | |
| mask = b.get("mask") | |
| if mask is not None: | |
| m = np.asarray(mask, dtype=bool) | |
| fill = np.zeros((*m.shape, 4), dtype=np.uint8) | |
| fill[m] = (*color, 90) | |
| overlay.alpha_composite(Image.fromarray(fill, "RGBA")) | |
| dr.rectangle([x1, y1, x2, y2], outline=color, width=3) | |
| label = f'{b.get("label", "?")} {b.get("score", 0):.2f}' | |
| dr.text((x1 + 3, max(0, y1 - 12)), label, fill=color) | |
| annotated = Image.alpha_composite(annotated.convert("RGBA"), overlay).convert("RGB") | |
| dr = ImageDraw.Draw(annotated) | |
| # subject box (thick white) | |
| subj = derive.subject_fixation(s.boxes, s.size).get("primary_subject", {}) | |
| if subj.get("box"): | |
| x1, y1, x2, y2 = [int(v) for v in subj["box"]] | |
| dr.rectangle([x1, y1, x2, y2], outline=(255, 255, 255), width=4) | |
| # outline of the largest mask | |
| masked = [b for b in s.boxes if b.get("mask") is not None] | |
| if masked: | |
| big = max(masked, key=lambda b: np.asarray(b["mask"]).sum()) | |
| poly = derive.outline_polygon(big["mask"], big["label"])["outline"] | |
| if len(poly) >= 6: | |
| pts = [(poly[j], poly[j + 1]) for j in range(0, len(poly) - 1, 2)] | |
| dr.line(pts + [pts[0]], fill=(255, 0, 128), width=2) | |
| depth_img = _colorize_depth(s.depth) if s.depth is not None else None | |
| return {"annotated": annotated, "depth": depth_img} | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| # Gradio callbacks | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| def _prep(image) -> Image.Image: | |
| im = image if isinstance(image, Image.Image) else Image.open(image) | |
| im = im.convert("RGB") | |
| if max(im.size) > MAX_DIM: | |
| im.thumbnail((MAX_DIM, MAX_DIM)) | |
| return im | |
| def run_single(image, vocab_choice, custom_vocab, tasks_sel, use_ocr, use_masks, | |
| use_depth, box_thr, text_thr, structurer_choice, captions_text, | |
| use_age, t_own, t_margin, dedup_iou, coord_space): | |
| if image is None: | |
| raise gr.Error("Upload or pick an image first.") | |
| im = _prep(image) | |
| vocab = _resolve_vocab(vocab_choice, custom_vocab) | |
| struct_id = STRUCTURERS.get(structurer_choice) | |
| caps = [c.strip() for c in (captions_text or "").splitlines() if c.strip()] | |
| results, structs_rows, raws_rows, audits, timing = gpu_extract( | |
| [im], vocab, float(box_thr), float(text_thr), bool(use_ocr), bool(use_masks), | |
| bool(use_depth), struct_id, [caps], bool(use_age), 1, 2, True) | |
| row = _assemble(results, structs_rows, raws_rows, audits, [im.size], | |
| float(t_own), float(t_margin), float(dedup_iou), coord_space, | |
| set(tasks_sel or []))[0] | |
| ov = row["overlays"] or {} | |
| return ( | |
| ov.get("annotated"), ov.get("depth"), | |
| row["prompt_fused"], row["fusion_confidence"], | |
| row["tasks_json"], row["tasks_valid"], row["fused_json"], | |
| row["struct"], (row["age_audit"] or {}), timing, | |
| _download_row(row), | |
| ) | |
| def run_batch(files, vocab_choice, custom_vocab, use_ocr, use_masks, use_depth, | |
| box_thr, text_thr, structurer_choice, use_age, t_own, t_margin, | |
| dedup_iou, coord_space, batch, gdino_batch): | |
| if not files: | |
| raise gr.Error("Upload at least one image.") | |
| files = files[:BATCH_CAP] | |
| ims = [_prep(f) for f in files] | |
| vocab = _resolve_vocab(vocab_choice, custom_vocab) | |
| struct_id = STRUCTURERS.get(structurer_choice) | |
| t0 = time.perf_counter() | |
| results, structs_rows, raws_rows, audits, timing = gpu_extract( | |
| ims, vocab, float(box_thr), float(text_thr), bool(use_ocr), bool(use_masks), | |
| bool(use_depth), struct_id, [[] for _ in ims], bool(use_age), | |
| int(batch), int(gdino_batch), False) | |
| rows = _assemble(results, structs_rows, raws_rows, audits, [im.size for im in ims], | |
| float(t_own), float(t_margin), float(dedup_iou), coord_space, None) | |
| wall = time.perf_counter() - t0 | |
| table, jsonl = [], [] | |
| for i, row in enumerate(rows): | |
| cls = row["tasks_json"].get("image_classification", {}) if row["tasks_json"] else {} | |
| n_ent = len(row["fused_json"].get("entities", [])) if isinstance(row["fused_json"], dict) else 0 | |
| nvalid = sum(1 for v in row["tasks_valid"].values() if v) | |
| table.append([i, cls.get("label", ""), n_ent, row["fusion_confidence"], | |
| f"{nvalid}/{len(row['tasks_valid'])}", (row["prompt_fused"] or "")[:90]]) | |
| jsonl.append({ | |
| "idx": i, "tasks_json": json.dumps(row["tasks_json"]), | |
| "tasks_valid": json.dumps(row["tasks_valid"]), | |
| "fused_json": json.dumps(row["fused_json"]), | |
| "prompt_fused": row["prompt_fused"], "fusion_confidence": row["fusion_confidence"], | |
| "proc_width": row["proc_width"], "proc_height": row["proc_height"], | |
| }) | |
| f = tempfile.NamedTemporaryFile("w", suffix=".jsonl", delete=False, encoding="utf-8") | |
| for r in jsonl: | |
| f.write(json.dumps(r) + "\n") | |
| f.close() | |
| summary = { | |
| "images": len(ims), "wall_s": round(wall, 2), | |
| "img_per_s": round(len(ims) / max(0.001, wall), 2), | |
| **{k: v for k, v in timing.items() if k != "n_images"}, | |
| } | |
| return table, summary, f.name | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| # UI | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| def _controls(): | |
| """Shared control widgets — returned so both tabs can wire them.""" | |
| vocab_choice = gr.Radio(list(VOCABS) + ["custom"], value="COCO-80", label="Detection vocab") | |
| custom_vocab = gr.Textbox(label="Custom phrases (comma-separated)", visible=False, | |
| placeholder="person, red circle, laptop") | |
| with gr.Row(): | |
| use_ocr = gr.Checkbox(True, label="OCR") | |
| use_masks = gr.Checkbox(True, label="SAM masks") | |
| use_depth = gr.Checkbox(True, label="Depth") | |
| with gr.Row(): | |
| box_thr = gr.Slider(0.05, 0.6, 0.30, step=0.01, label="box threshold") | |
| text_thr = gr.Slider(0.05, 0.6, 0.25, step=0.01, label="text threshold") | |
| structurer = gr.Radio(list(STRUCTURERS), value="off", label="Caption structurer") | |
| with gr.Row(): | |
| t_own = gr.Slider(0.0, 1.0, 0.60, step=0.01, label="t_own") | |
| t_margin = gr.Slider(0.0, 1.0, 0.25, step=0.01, label="t_margin") | |
| dedup_iou = gr.Slider(0.0, 1.0, 0.75, step=0.01, label="dedup_iou") | |
| coord_space = gr.Radio(COORD_SPACES, value="norm_0_1000", label="Fused-scene coord space") | |
| use_age = gr.Checkbox(False, label="Age-gate pre-filter (nateraw/vit-age-classifier)") | |
| vocab_choice.change(lambda c: gr.update(visible=(c == "custom")), | |
| vocab_choice, custom_vocab) | |
| return (vocab_choice, custom_vocab, use_ocr, use_masks, use_depth, box_thr, | |
| text_thr, structurer, coord_space, use_age, t_own, t_margin, dedup_iou) | |
| with gr.Blocks(title="Qwen Runner Vision — 12-task extraction + fusion") as demo: | |
| gr.Markdown( | |
| "# 🧩 Qwen Runner Vision\n" | |
| "Deterministic **12-task** extraction + **fusion** — stick an image in, get the " | |
| "full JSON readout (task JSONs + `FusedScene` + fused prompt). Specialists run on " | |
| "**ZeroGPU**; fusion is CPU-only. Every option below is a live toggle." | |
| ) | |
| with gr.Tab("Single image"): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| img_in = gr.Image(type="pil", label="Image", height=320) | |
| tasks_sel = gr.CheckboxGroup(DET_TASKS, value=DET_TASKS, | |
| label="Tasks to show (all 12 always computed)") | |
| gr.CheckboxGroup(VLM_TASKS, label="VLM/DEFER (no deterministic builder)", | |
| interactive=False) | |
| captions = gr.Textbox(lines=3, label="Captions (one per line — enrich fusion)", | |
| placeholder="a woman with long red hair in a blue coat") | |
| with gr.Accordion("Settings", open=False): | |
| ctl = _controls() | |
| run_b = gr.Button("Extract", variant="primary") | |
| with gr.Column(scale=1): | |
| with gr.Row(): | |
| annotated = gr.Image(label="Detections · masks · subject · outline", height=280) | |
| depth_img = gr.Image(label="Depth (near → far)", height=280) | |
| prompt_out = gr.Textbox(label="Fused prompt", lines=3) | |
| conf_out = gr.Number(label="Fusion confidence") | |
| dl = gr.File(label="Download row (JSON)") | |
| with gr.Accordion("Full JSON readout", open=True): | |
| tasks_out = gr.JSON(label="tasks_json (12 tasks)") | |
| with gr.Row(): | |
| valid_out = gr.JSON(label="tasks_valid") | |
| struct_out = gr.JSON(label="caption structs") | |
| fused_out = gr.JSON(label="fused_json (FusedScene)") | |
| with gr.Row(): | |
| age_out = gr.JSON(label="age_audit") | |
| timing_out = gr.JSON(label="timing") | |
| (vocab_choice, custom_vocab, use_ocr, use_masks, use_depth, box_thr, | |
| text_thr, structurer, coord_space, use_age, t_own, t_margin, dedup_iou) = ctl | |
| ex_dir = os.path.join(os.path.dirname(__file__), "examples") | |
| if os.path.isdir(ex_dir): | |
| ex_imgs = [[os.path.join(ex_dir, f)] for f in sorted(os.listdir(ex_dir)) | |
| if f.lower().endswith((".png", ".jpg", ".jpeg"))] | |
| if ex_imgs: | |
| gr.Examples(ex_imgs, inputs=img_in, label="Examples") | |
| run_b.click( | |
| run_single, | |
| inputs=[img_in, vocab_choice, custom_vocab, tasks_sel, use_ocr, use_masks, | |
| use_depth, box_thr, text_thr, structurer, captions, use_age, | |
| t_own, t_margin, dedup_iou, coord_space], | |
| outputs=[annotated, depth_img, prompt_out, conf_out, tasks_out, valid_out, | |
| fused_out, struct_out, age_out, timing_out, dl], | |
| ) | |
| with gr.Tab("Batch (the batched structure)"): | |
| gr.Markdown( | |
| f"Upload up to **{BATCH_CAP}** images → batched `solidify_batch` on ZeroGPU " | |
| "(`gdino_batch=2`, GDINO anti-scales) + per-image CPU fusion. Throughput mirrors " | |
| "`runs/extract_throughput_results.md`." | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| files_in = gr.Files(label="Images", file_types=["image"]) | |
| with gr.Accordion("Settings", open=False): | |
| bctl = _controls() | |
| with gr.Row(): | |
| batch_sl = gr.Slider(1, 24, 16, step=1, label="extract_batch") | |
| gdino_sl = gr.Slider(1, 8, 2, step=1, label="gdino_batch (keep ~2)") | |
| run_batch_b = gr.Button("Run batch", variant="primary") | |
| with gr.Column(scale=1): | |
| batch_table = gr.Dataframe( | |
| headers=["#", "label", "entities", "fusion_conf", "valid", "prompt…"], | |
| label="Per-image results", wrap=True) | |
| batch_summary = gr.JSON(label="Throughput") | |
| batch_dl = gr.File(label="Download rows (JSONL)") | |
| (b_vocab, b_custom, b_ocr, b_masks, b_depth, b_box, b_text, b_struct, | |
| b_coord, b_age, b_town, b_tmargin, b_dedup) = bctl | |
| run_batch_b.click( | |
| run_batch, | |
| inputs=[files_in, b_vocab, b_custom, b_ocr, b_masks, b_depth, b_box, b_text, | |
| b_struct, b_age, b_town, b_tmargin, b_dedup, b_coord, batch_sl, gdino_sl], | |
| outputs=[batch_table, batch_summary, batch_dl], | |
| ) | |
| if __name__ == "__main__": | |
| demo.queue().launch() | |