Spaces:
Running
A newer version of the Gradio SDK is available: 6.20.0
Field Notes β Building Tabras
What I learned building a small-model card duel for the Build Small hackathon.
1. ZeroGPU is a GPU-sharing mechanism, not a hosted GPU provider
This was my biggest wall, and I hit it late. I built Tabras assuming a Hugging Face
ZeroGPU Space would behave like a hosted GPU I could just run my models on. It does
not. ZeroGPU time-slices a shared GPU and only grants it inside @spaces.GPU
functions β and it ships args/returns between processes by pickling them. That
breaks the moment you try to hand it a trust_remote_code model (MiniCPM) or a
diffusers pipeline: they aren't picklable, and low-level CUDA init isn't allowed in
the main process at all. I burned real time learning this through a wall of
PicklingError and "CUDA init reached" tracebacks.
The last-minute fix was to re-architect: make the Space a thin HTTP client and move every model onto Modal GPU endpoints (MiniCPM for cards, Nemotron for the boss, SDXL-Turbo for art), each autoscaled on its own dedicated GPU. The Space just POSTs prompts and renders the results. Lesson: understand the execution model of your compute before you design around it, not after.
Crucially, Tabras still runs fully local / off-grid β a MODE switch flips
between LOCAL (in-process Transformers/Diffusers, or a local llama.cpp server for
MiniCPM) and MODAL (the hosted endpoints). The README has the local instructions.
Small and local was always the point, and keeping that path alive mattered to me.
2. Small models: surprisingly powerful, with sharp edges
- Image models punch way above their size. SDXL-Turbo, in ~4 denoising steps, produced genuinely striking card art and theme backgrounds. The visual identity of the whole game is carried by a tiny, fast diffusion model.
- Nemotron was the standout for agentic / tool-calling work. Giving it the public board state and a constrained JSON action schema, it reliably reads the situation and returns valid, sensible plays. A 4B model running a competent opponent was the part I expected to be flaky and wasn't.
- The text model owns meaning, not structure. MiniCPM writes evocative names and
flavor, but it leans on lazy patterns ("Fire Card"), leaks prompt vocabulary, and
truncates its own JSON. The single highest-leverage fix was reordering the requested
JSON so
effectsandnamecome first β they survive a token cutoff that used to silently collapse a whole card to a fallback. Design around what the model is bad at.
3. Perceived latency beats raw latency
Small/local inference isn't instant, and the variance (some packs fast, some slow) looked worse than a consistent wait. The fixes that made the demo feel good were mostly UX, not compute:
- A minimum loading window on each draft transition, so every pick shows the same deliberate "forging" beat β that uniform pause hides the slow ones behind the same animation the fast ones use.
- Prefetching every branch of the draft during idle time (the reveal/rules screens, and while you read the current pack), so whichever card you pick, its next pack is already generating.
- Pre-baking static art for the fixed backbone cards once and bundling it, so they never spend a live generation or shimmer.
A consistent 2s always beats an unpredictable 0β8s.
4. Card-game design is hard β and a ton of fun
Balancing a generative card game is genuinely difficult. The core principle that made it tractable: the LLM owns meaning, the engine owns math. The model picks which effects a card has and writes its identity; deterministic Python prices every number against a point budget, so cards are balanced-by-construction no matter what the model invents. The draft is deck-aware β it reads the build you're assembling and shapes packs toward it, sometimes dangling a tempting off-archetype card. Getting that loop to feel fair and surprising was the most fun part of the whole project.
5. Ambition under a deadline
This was an ambitious build for the time window β three small models, a custom UI, a last-minute compute re-architecture, and a real game loop. The thing that saved me was treating the demo video as the deliverable and optimizing the local recording surface hard, rather than betting everything on a flawless live Space. I'm proud of how much of it came together, and I had a lot of fun doing it.