AbstractPhil's picture
Deploy: 12-task vision extraction + fusion ZeroGPU showcase
fed954e verified
Raw
History Blame Contribute Delete
31.3 kB
"""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:
@staticmethod
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()
@torch.no_grad()
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))
@spaces.GPU(duration=_gpu_duration)
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()