| # NumZoo β Agent Guidelines |
|
|
| ## What this project is |
| Kids mental math app. Correct answers earn AI-generated cute animal images (FLUX.2-klein-4B). |
| Stack: Python Β· Gradio Β· diffusers Β· HuggingFace Spaces (ZeroGPU). |
|
|
| ## Mandatory dev loop |
| Run these scripts in order before every push. Fix failures before proceeding. |
|
|
| | Step | Command | Must pass | |
| |------|---------|-----------| |
| | Syntax | `bash scripts/syntax.sh` | No syntax errors | |
| | Imports | `bash scripts/check_imports.sh` | No import errors | |
| | SDK version | `bash scripts/check_sdk.sh` | README sdk_version == local gradio | |
| | Run | `bash scripts/run.sh` | App starts, no errors in logs | |
| | Test | `bash scripts/test.sh` | All Puppeteer checks pass, screenshots look correct | |
| |
| Full loop shortcut: `bash scripts/dev.sh` (runs steps 1β4, then run test.sh manually). |
| |
| **Never push if any step fails.** |
| |
| ## Key files |
| |
| | File | Role | |
| |------|------| |
| | `app.py` | Gradio UI, game logic, panel visibility, event wiring | |
| | `image_generator.py` | FLUX.2-klein-4B pipeline, persistent cache setup, emojiβtext prompt builder | |
| | `math_engine.py` | Question generation, level names & thresholds | |
| | `requirements.txt` | Python dependencies β never include `gradio` here | |
| | `README.md` | HF Spaces config (frontmatter `sdk_version`) + user docs | |
| | `scripts/` | Dev loop automation scripts | |
|
|
| ## Architecture |
|
|
| **3-panel flow:** Welcome β Emoji Picker β Quiz |
|
|
| **Game state dict keys:** |
| `name`, `level`, `score`, `streak`, `correct_since_reward`, `level_correct`, |
| `question`, `answer`, `selected_animals`, `selected_places`, |
| `reward_count`, `pregenerate_next`, `generate_now`, `pending_reward_id` |
|
|
| **Reward trigger:** every `REWARD_EVERY = 3` correct answers. |
|
|
| **Pre-generation flow (async image UX):** |
| 1. "Let's go!" β `start_game()` β adds locked placeholder for reward #1 in collection β |
| `.then(pregenerate_image)` starts background generation immediately |
| 2. `pregenerate_image` stores PIL image in `hidden_image` (gr.Image) and base64 data URL in |
| `hidden_data_url` (gr.Textbox), sets `state["pending_reward_id"]` |
| 3. When user earns a reward: `check_answer` receives `hidden_image` + `hidden_data_url` as inputs. |
| - If pre-image matches reward_id β show immediately + unlock in collection + kick off next pregenerate |
| - If not ready β show LOADER_HTML + set `generate_now=True` |
| 4. `.then(generate_on_demand)` β generates on-demand if `generate_now`, unlocks collection |
| 5. `.then(pregenerate_image)` β generates next reward in background |
|
|
| **Collection system:** |
| - `collection_trigger = gr.HTML("")` receives JSON-encoded action payloads |
| - JS polls `#numzoo-coll-data` element every 200ms for `{ts, actions:[{action,id,src}]}` |
| - Actions: `add-locked` (locked placeholder), `unlock` (reveal image), `failed` (remove) |
| - Images stored in localStorage as base64 JPEGs (key: `numzoo_collection_v2`) |
|
|
| **Loader fix:** Use value-swapping (`""` = hidden, `LOADER_HTML` = shown) instead of `visible` flag. |
| This is more reliable in Gradio 6 than toggling visibility on gr.HTML components. |
|
|
| ## Gradio rules |
| - Use `gr.update(visible=True/False)` for visibility β never return bare booleans |
| - Theme and CSS go in `demo.launch(css=..., theme=...)`, NOT in `gr.Blocks()` |
| - `gr.Group` has no `.change()` event |
| - Never put `gradio` in `requirements.txt` β HF Spaces pins it via `sdk_version` in README |
| - Never put `audioop-lts` in `requirements.txt` β HF Spaces runs Python 3.10 where `audioop` is stdlib; `audioop-lts` requires >=3.13 and will break the build |
| - Keep `sdk_version` in README in sync with local gradio (run `scripts/check_sdk.sh`) |
|
|
| ## Python runtime |
| - **Local:** `~/miniforge3/bin/python3` (arm64, Python 3.13, torch 2.12, MPS) |
| - **HF Spaces:** Python 3.13, ZeroGPU (A100 via `@spaces.GPU`) |
| - Do not use system Python (`/usr/local/bin/python3`) β it's x64 Rosetta, incompatible |
|
|
| ## Image generation |
| - Model: `black-forest-labs/FLUX.2-klein-4B` (Apache 2.0 β accept license on HF before first run) |
| - Same model local + HF Spaces. 4 steps, `guidance_scale=1.0`, 512Γ512 |
| - Local: float16 on MPS (needs ~13GB, fits in 32GB unified mem) Β· HF ZeroGPU: ~fast on A100 |
| - Pipeline class: `Flux2KleinPipeline` (diffusers β₯ 0.38) |
| - Always wrap in try/except β return `(None, "")` on failure |
| - `IS_HF_SPACE = os.environ.get("SPACE_ID") is not None` controls ZeroGPU decorator |
| - Logs generation time (`β
Generated in Xs`) |
|
|
| ## Persistent model cache (HF Spaces) |
| - `/data/hf_cache_v4` mount caches the model across restarts (set in Space Settings β Storage) |
| - Setup is **bulletproof**: poisoned/partial cache is auto-wiped & re-downloaded; |
| any `/data` failure falls back to ephemeral container cache; import never crashes |
| - If the cache tree gets poisoned again (`[Errno 20] Not a directory`), bump the |
| path suffix (`_v4` β `_v5`) to escape it β established repo pattern |
| - `NUMZOO_STYLE` constant in `image_generator.py` is the single source of truth for |
| the art style; `scripts/generate_dataset.py` imports it so training captions match |
|
|
| ## HuggingFace deployment |
| - Space: `https://huggingface.co/spaces/Goumsss/numzoo` |
| - Push = redeploy. Rebuilds automatically. |
| - ZeroGPU: set in Space Settings β Hardware (requires HF Pro or hackathon grant) |
|
|
| ## UI/UX principles |
| - English only, minimal text β words not sentences (kids with limited English) |
| - Emoji-heavy feedback (`β
Great! π₯π₯` not full sentences) |
| - Never crash visibly β all handlers wrapped in try/except with safe fallbacks |
|
|