Spaces:
Running on Zero
A newer version of the Gradio SDK is available: 6.27.0
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What this is
A Gradio app deployed to a Hugging Face Space (ZeroGPU), with dependencies managed by uv and continuous deployment via GitHub Actions. The app (app.py) serves the sVLM-Council β small (~1β9B) vision-language models answering the same image+prompt side by side β as a UI, REST API, and MCP server. Space: GF-John/svlm-council.
Model comparison scripts
Alongside the template app.py, the repo has three self-contained PEP 723 uv scripts:
ocr_arena.py (full-image OCR across backends), extract_arena.py (targeted field
extraction with N/A-abstention guardrails and -F format directives), and
svlm_council.py (small VLMs run locally via transformers; importable query() used by
extract_arena). Each declares its own deps inline β run with uv run [--env-file .env] <script>.
When adding, replacing, or re-evaluating models, start from MODEL_RESEARCH.md β it holds
the verified candidate tables (hosted + local), benchmark snapshots, provider-reliability
notes, per-model transformers quirks, prompting findings, and verified model licenses (with
per-scenario implications), plus the endpoints to re-check
since that data goes stale. Update it after any new model research.
Commands
- Install deps:
uv sync(creates/updates.venvfrompyproject.toml+uv.lock) - Add a dependency:
uv add <package>(updatespyproject.tomlanduv.lock) - Run the app locally:
uv run app.py(launches Gradio onhttp://127.0.0.1:7860, with Swagger docs at/docsand an MCP server enabled) requirements.txtis hand-curated (loose pins; nogradio/spacesβ the Space's SDK provides those). Don't regenerate it withuv export; when app code gains a dependency, append it manually.
There are no tests or linters configured.
Deployment
Push to main triggers .github/workflows/deploy_to_hf_space.yaml, which:
- Generates
requirements.txtfrompyproject.tomlviauv exportonly when it doesn't already exist (the Space itself has nopyproject.toml; HF Spaces readrequirements.txt), then commits it back. - Pushes the repo to the HF Space β a normal push by default, or a force push if the
FORCE_PUSHsecret is set (a force push overwrites the Space's history, so use it only if you are the sole contributor).
Configuration is driven entirely by GitHub secrets (no workflow edits needed):
HF_TOKEN(required) β auth token. If absent the push step is skipped (the workflow still succeeds).HF_USERNAMEandSPACE_NAME(required) β interpolated into the push URL.../spaces/$HF_USERNAME/$SPACE_NAME.FORCE_PUSH(optional) β any value enables force push.
Also update the YAML frontmatter in README.md (title, emoji, sdk_version, short_description, etc.) β HF Spaces reads its config from there.
Binary assets
HF Spaces rejects raw binaries in the git tree β image/video/audio/model-weight files must go through Git LFS / Xet (git lfs track), not be committed directly. Common binary extensions are git-ignored in .gitignore to prevent accidental commits; if a binary genuinely needs to ship, track it with LFS rather than removing the ignore rule.
App architecture (app.py)
app.py is a gr.TabbedInterface with two tabs, each a gr.Interface = REST endpoint = MCP tool:
Council (council_query) serves the council from svlm_council.py β it imports the COUNCIL registry from there but has its own ZeroGPU load/infer path (the CLI's MPS single-slot eviction cache doesn't apply on the Space). Adding/changing a council member means editing COUNCIL in svlm_council.py; app.py picks it up automatically.
ExtractArena (extract_fields) reuses extract_arena.py as a library: its prompt building (build_prompt), reply normalization (_normalize_value/_parse_json_reply), and remote backends (extract_arena.MODELS[name].fn β HF Inference Providers + the moondream Space) run verbatim in the main process (unbilled); local council names instead route through the resident-model _gpu_run_council path (never extract_arena's MPS loader). import extract_arena must stay after _load_dotenv() β it reads HF_TOKEN at import time. HF_TOKEN is optional: without it the hosted backends fail per-model (logged) while the rest run. Result shape mirrors ExtractResult: {"model", "value", "values", "raw", "latency_s", "error"} (value normalized single-field answer with "" = abstained; values for multi-field). Extract knobs: MAX_EXTRACT_MODELS (default 3), DEFAULT_EXTRACT_MODELS (default qwen3.5,muse-glimmer).
- Startup (module level, unbilled): every enabled member is loaded via
AutoProcessor/AutoModelForImageTextToTextand moved.to("cuda")β on ZeroGPU the main process runs a CUDA emulation mode, so module-level CUDA placement is the officially recommended pattern (lazy-loading inside@spaces.GPUis discouraged). All four members β 37GB bf16, fitting the defaultlargeZeroGPU slice (48GB VRAM;xlarge= 96GB at 2Γ quota). No flash-attn and no quantization; on CUDA every member is forced to sdpa attention (a spec'deagerβ hunyuan-ocr β would materialize seqΒ² float32 attention transients that OOM the VRAM left beside the resident council). council_queryis the public function = REST endpoint = MCP tool. It validates (returns a structured single-element error list instead of raising, so MCP clients get something actionable), then delegates to_gpu_run_council, the only@spaces.GPUfunction β its dynamicdurationcallable scales with the number of selected models (the requested duration is ZeroGPU's quota gate, so keep it tight; tune from Space logs).- Result shape:
list[dict], one per model:{"model", "text", "latency_s", "error"}; per-model failures are isolated. - Concurrency: no locking in the code; Gradio's queue does it.
concurrency_limit=2on the Council interface and4on ExtractArena (each concurrent_gpu_run_councilcall forks its own ZeroGPU worker and GPU, so the ceiling is the GPU pool, not VRAM; remote extract backends are I/O-bound and overlap freely).app.queue(max_size=16)is explicit because ZeroGPU forcesmax_size=1when unset, which 503s ("Queue is full") every caller beyond the running six plus one waiter. Within one request models run sequentially. - MCP docstring is assigned to
council_query.__doc__after the def (f-string β it embeds the runtime roster, max, and the MODEL_RESEARCH.md links so calling agents can pick models). It follows thegradio-mcp-tool-docstringsskill rules: schema + URLs in the description prose, one-lineArgs:entries, noReturns:section. Re-verify with the skill'sdump_mcp_schema.pyafter editing it. - Env knobs:
MAX_COUNCIL_MODELS(default 3),DEFAULT_COUNCIL_MODEL(defaultqianfan-ocr),COUNCIL_MEMBERS(comma-separated subset to load;COUNCIL_MEMBERS=hunyuan-ocr uv run app.pyis the lightweight local dev run β the full council won't fit in 32GB local RAM),MAX_IMAGE_PIXELS(default 3,000,000 β inputs above this are downscaled in the main process before inference; hunyuan-ocr's processor has no resolution cap of its own, and its eager attention OOMs on huge uploads without this guard). - Version floors: torch is pinned
>=2.10,<2.12because ZeroGPU only supports torch up to 2.11; gradio is 6.x (gradio[mcp]extra required formcp_server=True).