numzoo / CLAUDE.md
goumsss's picture
Docs: update CLAUDE.md + README for FLUX.2-klein-4B
2b1f846
|
Raw
History Blame Contribute Delete
5.5 kB
# 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