Spaces:
Sleeping
Sleeping
File size: 6,466 Bytes
c42f4ef | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | """The ZeroGPU Space: transformers VLM backend and the Gradio app, driven over its API with gradio_client.
Uses a tiny random Qwen3-VL built on disk (tests/tiny_vlm.py) so nothing is downloaded; set
MRAG_VLM_ID to a real model (e.g. Qwen/Qwen3-VL-2B-Instruct) to run the same tests against it.
"""
import os
import socket
import sys
from pathlib import Path
import pytest
from conftest import ROOT
pytest.importorskip("transformers")
@pytest.fixture(scope="session")
def vlm_id(tmp_path_factory):
if os.environ.get("MRAG_VLM_ID"):
return os.environ["MRAG_VLM_ID"]
try:
from transformers import Qwen3VLForConditionalGeneration # noqa: F401
except ImportError:
pytest.skip("transformers without Qwen3-VL (needs >=4.57)")
sys.path.insert(0, str(ROOT / "tests"))
import tiny_vlm
return str(tiny_vlm.build(tmp_path_factory.mktemp("tiny_qwen3vl")))
@pytest.fixture(scope="session")
def vlm(vlm_id):
from mrag.hf_vlm import HFVLM
return HFVLM(vlm_id)
def test_vlm_image_tokens_match_vision_grid(vlm):
from PIL import Image
ims = [Image.new("RGB", (900, 500), "red"), Image.new("RGB", (300, 600), "blue")]
inp = vlm.build_inputs("system", "Question: what colour?", ims)
grid = inp["image_grid_thw"]
merge = vlm.processor.image_processor.merge_size
n_img_tokens = int((inp["input_ids"] == vlm.model.config.image_token_id).sum())
assert grid.shape[0] == 2 and n_img_tokens == int(grid.prod(-1).sum()) // merge ** 2
def test_vlm_streams_text(vlm):
from PIL import Image
pieces = list(vlm.stream("system", "Question: hi", [Image.new("RGB", (64, 64))], max_new_tokens=8))
assert pieces and all(isinstance(p, str) for p in pieces)
def test_vlm_errors_are_raised_not_hung(vlm):
with pytest.raises(Exception):
list(vlm.stream("system", "x", [], max_new_tokens=-5))
def test_generator_transformers_backend(vlm, tmp_path):
from PIL import Image
from mrag.config import Config
from mrag.generate import Generator, ImageHit, TextHit
p = tmp_path / "a.png"
Image.new("RGB", (3000, 1500), "green").save(p)
g = Generator(Config(llm_backend="transformers", max_new_tokens=6, max_image_side=256), vlm=vlm)
out = g.answer("q?", [TextHit("1", "fact", "/d/x.pdf", 1, "text", 1.0)],
[ImageHit("i", str(p), "/d/x.pdf", 1, "", 0.3)])
assert isinstance(out, str)
def _free_port():
s = socket.socket()
s.bind(("127.0.0.1", 0))
port = s.getsockname()[1]
s.close()
return port
@pytest.fixture(scope="module")
def client(vlm_id, tmp_path_factory):
pytest.importorskip("gradio_client")
os.environ["MRAG_VLM_ID"] = vlm_id
os.environ.setdefault("MRAG_TEXT_BACKEND", "wordllama")
os.chdir(tmp_path_factory.mktemp("space")) # demo_docs is built next to app.py; keep tests clean
sys.path.insert(0, str(ROOT))
import app
from gradio_client import Client
port = _free_port()
app.demo.queue().launch(server_port=port, prevent_thread_lock=True, quiet=True)
yield Client(f"http://127.0.0.1:{port}/", verbose=False), app
app.demo.close()
def test_app_demo_docs_and_streamed_answer(client):
c, app = client
status = c.predict(api_name="/load_demo")
assert "7 file(s)" in status and "report.pdf" in status
chat, gallery, sources, box, *_ = c.predict(
"How long is the warranty on the Nimbus R2?", [], 6, 3, True,
api_name="/prepare_answer")
assert len(chat) == 2 and chat[0]["role"] == "user"
assert "36-month" in sources and "report.pdf p.3" in sources
assert gallery and "report.pdf p.3" in gallery[0]["caption"]
job = c.submit(64, chat, api_name="/vlm_answer")
outputs = list(job)
final = job.result()
assert outputs and final[-1]["role"] == "assistant" and final[-1]["content"]
assert box["value"] == "" and box["interactive"] is False
# gradio_client calls endpoints individually; the browser runs this chained event.
c.predict(api_name="/finish_answer")
def test_app_upload_and_session_isolation(client, corpus):
from gradio_client import Client, handle_file
c, _ = client
fresh = Client(c.src, verbose=False)
with pytest.raises(Exception, match="Index some files"):
fresh.predict("a cat?", [], 6, 3, True, api_name="/prepare_answer")
status, _ = fresh.predict([handle_file(str(corpus / "images" / "office_cat.jpg"))], api_name="/index_files")
assert "1 file(s)" in status and "office_cat.jpg" in status
chat, gallery, sources, *_ = fresh.predict("show me a picture of a cat", [], 6, 3, True, api_name="/prepare_answer")
assert gallery and "office_cat.jpg" in gallery[0]["caption"]
assert "report.pdf" not in sources
final = fresh.predict(64, chat, api_name="/vlm_answer")
assert final[-1]["role"] == "assistant"
fresh.predict(api_name="/finish_answer")
def test_app_busy_guard_and_failure_cleanup(client):
c, app = client
chat, *_ = c.predict("Who is the CEO?", [], 6, 0, False, api_name="/prepare_answer")
with pytest.raises(Exception, match="still running"):
c.predict(api_name="/reset")
# This handler also runs when ZeroGPU rejects a request or generation fails.
c.predict(api_name="/finish_answer")
with pytest.raises(Exception, match="Submit a question"):
c.predict(64, chat, api_name="/vlm_answer")
c.predict(api_name="/finish_answer")
def test_app_cumulative_upload_limit(client, tmp_path):
from gradio_client import Client, handle_file
c, app = client
fresh = Client(c.src, verbose=False)
paths = []
for i in range(app.MAX_FILES + 1):
p = tmp_path / f"file{i}.txt"
p.write_text(f"Document {i}: the robot payload is 25 kilograms.")
paths.append(handle_file(str(p)))
status, _ = fresh.predict(paths[:app.MAX_FILES], api_name="/index_files")
assert f"{app.MAX_FILES} file(s)" in status
with pytest.raises(Exception, match="indexed files per session"):
fresh.predict(paths[-1:], api_name="/index_files")
fresh.predict(api_name="/reset")
def test_app_generation_is_success_only_and_serialized(client):
_, app = client
gpu_id = next(i for i, fn in app.demo.fns.items() if fn.fn == app.vlm_answer)
binding = next(d for d in app.demo.config["dependencies"] if d["id"] == gpu_id)
assert binding["trigger_only_on_success"] is True
assert app.demo.fns[gpu_id].concurrency_limit == 1
|