asanwari/secret-student-mock
Secret Student
Secret Student is a Gradio-hosted retro learning game for the Hugging Face Build Small Hackathon. A player registers as a school kid with a secret identity, learns a topic at school, gets a mission call at home, and fights a boss at {player_name}'s Grandma's House.
The app uses a custom Phaser frontend embedded in a Gradio shell, with FastAPI routes for game data and a SQLite database for persistence.
Run Locally
uv sync
cp .env.example .env
LLM_PROVIDER=mock uv run uvicorn main:app --reload --host 0.0.0.0 --port 7860Open:
http://127.0.0.1:7860To open the game from another device on the same Wi-Fi network, use the laptop's local network address instead, for example:
http://192.168.1.123:7860The address may change when the laptop reconnects to Wi-Fi. On macOS, find it under System Settings > Wi-Fi > Details > TCP/IP, or run ifconfig en0.
Mock mode is fully playable without a model endpoint.
Model Configuration
Choose the model runtime at application startup:
LLM_RUNTIME=mock # no external model; deterministic demo content
LLM_RUNTIME=external # Modal or another OpenAI-compatible endpoint
LLM_RUNTIME=embedded_llamacpp # llama.cpp runs beside the app in one container
LLM_RUNTIME=embedded_dual_llamacpp # separate text and vision llama.cpp serversLLM_PROVIDER remains supported for older local .env files, but LLM_RUNTIME takes precedence.
External Or Modal
The external mode works with Modal, a public llama.cpp server, or another OpenAI-compatible chat-completions endpoint:
Nemotron example:
LLM_RUNTIME=external
LLM_BASE_URL=https://your-modal-app.modal.run
LLM_API_KEY=replace-with-the-model-server-key
LLM_MODEL=ggml-org/NVIDIA-Nemotron-3-Nano-Omni:Q4_K_MMiniCPM-V 4.6 example, matching the model used during local development:
LLM_RUNTIME=external
LLM_BASE_URL=https://your-public-minicpm-endpoint
LLM_API_KEY=replace-with-the-model-server-key
LLM_MODEL=openbmb/MiniCPM-V-4.6Deploy the included Modal llama.cpp server:
uv sync --extra modal
modal setup
modal secret create secret-student-llm \
LLAMA_ARG_API_KEY=replace-with-a-long-random-key \
HF_TOKEN=hf_your_token
uv run --extra modal modal deploy scripts/deploy_llamacpp_modal.pyThe deploy command prints the URL to use as LLM_BASE_URL. The Modal script defaults to an L40S, persistent model caches, and the pinned ghcr.io/ggml-org/llama.cpp:server-cuda-b9049 image. Override its model or GPU with MODAL_LLAMA_CPP_MODEL_REF and MODAL_LLAMA_CPP_GPU before deployment. For MiniCPM-V on Modal, set MODAL_LLAMA_CPP_MODEL_REF to a compatible MiniCPM-V 4.6 GGUF repository and quantization before running modal deploy.
Embedded llama.cpp
The Docker image contains both Secret Student and llama-server. The startup supervisor launches and health-checks the model before serving the game:
Nemotron example:
LLM_RUNTIME=embedded_llamacpp
LLAMA_CPP_MODEL_REF=ggml-org/NVIDIA-Nemotron-3-Nano-Omni:Q4_K_M
LLAMA_CPP_API_KEY=replace-with-a-long-random-key
LLAMA_CPP_CTX_SIZE=8192
LLAMA_CPP_GPU_LAYERS=999
LLAMA_CPP_STARTUP_TIMEOUT=900
LLM_MODEL=ggml-org/NVIDIA-Nemotron-3-Nano-Omni:Q4_K_MMiniCPM-V 4.6 example:
LLM_RUNTIME=embedded_llamacpp
LLAMA_CPP_MODEL_REF=your-minicpm-v-4.6-gguf-repository:your-quantization
LLAMA_CPP_API_KEY=replace-with-a-long-random-key
LLAMA_CPP_CTX_SIZE=8192
LLAMA_CPP_GPU_LAYERS=999
LLM_MODEL=openbmb/MiniCPM-V-4.6LLAMA_CPP_MODEL_REF tells llama.cpp which GGUF package to download. LLM_MODEL is the model identifier sent in OpenAI-compatible API requests. They may differ. For handwritten image verification, the selected MiniCPM-V GGUF package must include or reference its multimodal projector and be supported by the pinned llama.cpp build.
The Space must use regular GPU hardware for this mode. Hugging Face ZeroGPU is available only to Gradio SDK Spaces using dynamically decorated GPU functions; it cannot provide a long-lived GPU to this Docker supervisor. Persistent Space storage is recommended because model caches live under /data.
Dual-model Space runtime
The recommended L4 configuration uses Qwen3-8B for lesson generation and teacher chat, and MiniCPM-V only for handwritten-answer extraction:
LLM_RUNTIME=embedded_dual_llamacpp
LLAMA_CPP_MODEL_PATH=/data/models/qwen3-8b/Qwen3-8B-Q4_K_M.gguf
VISION_LLAMA_CPP_MODEL_PATH=/data/models/minicpm-v-4_5/MiniCPM-V-4_5-Q4_K_M.gguf
VISION_LLAMA_CPP_MMPROJ_PATH=/data/models/minicpm-v-4_5/mmproj-model-f16.ggufUpload those completed files to the mounted bucket once. The runtime reads them directly on every restart. Fallback -hf downloads use /tmp/llama.cpp because bucket mounts do not support the atomic cache rename used by llama.cpp.
The preload helper is designed for a Hugging Face Job or another machine where the bucket is mounted at /data:
uv run python scripts/preload_space_models.pyIt downloads to local temporary storage first and then copies completed files to /data/models, safely resuming by checking existing file sizes. All selections can be changed without editing code. The runtime reads LLM_MODEL, LLAMA_CPP_MODEL_REF, LLAMA_CPP_MODEL_PATH, VISION_LLM_MODEL, VISION_LLAMA_CPP_MODEL_REF, VISION_LLAMA_CPP_MODEL_PATH, and VISION_LLAMA_CPP_MMPROJ_PATH. The preload helper accepts matching CLI options and the TEXT_MODEL_REPO, TEXT_MODEL_FILE, TEXT_MODEL_DESTINATION, VISION_MODEL_REPO, VISION_MODEL_FILE, VISION_MODEL_DESTINATION, VISION_MMPROJ_FILE, and VISION_MMPROJ_DESTINATION environment variables.
Run uv run python set_env_for_space.py --runtime embedded_dual_llamacpp to configure both local servers. Use empty --llama-model-path and --vision-llama-model-path values only when intentionally using ephemeral Hub downloads instead of preloaded bucket files.
For local NVIDIA Docker testing with a separate llama.cpp service:
docker compose -f compose.yaml -f compose.llamacpp.yaml up --buildThe lesson generator requests structured JSON and parses it into Pydantic classes. Text and numeric answers are checked deterministically first. Drawn answers are sent to the vision model only when the player submits notebook handwriting.
LLM Traces
Every OpenAI-compatible model call records its request, full response, parse or repair step, schema validation, timing, and final status. For local debugging:
TRACE_DESTINATION=local
TRACE_DIR=debug_tracesFailed API responses include a trace ID. Find the matching JSON file under debug_traces/; malformed model JSON is retained there before any automatic repair is attempted. Image data is replaced by a hash and API keys are never written to traces.
To publish completed traces to a Hugging Face dataset repository instead:
TRACE_DESTINATION=hub
TRACE_HUB_REPO_ID=your-name/secret-student-traces
TRACE_HUB_TOKEN=hf_your_write_token
TRACE_HUB_PRIVATE=true
TRACE_HUB_INCLUDE_CONTENT=falseHub traces remove the internal user ID. Prompts and raw completions are also removed unless TRACE_HUB_INCLUDE_CONTENT=true; review traces carefully before enabling that option, especially when testing with real learners.
Key Routes
POST /api/auth/registerPOST /api/auth/loginGET /api/mePOST /api/lesson/startGET /api/lesson/{lesson_id}POST /api/lesson/{lesson_id}/askPOST /api/quiz/submitPOST /api/boss/startPOST /api/boss/submitPOST /api/state/location
Tests
uv run pytestHugging Face Space
This repository is configured as a Docker Space. Set one of the following in Settings > Variables and secrets.
After a push to the Space's main branch, Hugging Face automatically rebuilds the Docker image and runs its CMD, which starts python -m app.runtime. A Factory reboot is useful after changing Variables or Secrets.
Modal/external variables:
LLM_RUNTIME=external
LLM_BASE_URL=https://your-modal-app.modal.run
LLM_MODEL=ggml-org/NVIDIA-Nemotron-3-Nano-Omni:Q4_K_M
# Or: openbmb/MiniCPM-V-4.6Modal/external secrets:
LLM_API_KEY=your-model-server-key
APP_SECRET=your-game-session-secretEmbedded variables:
LLM_RUNTIME=embedded_llamacpp
LLAMA_CPP_MODEL_REF=your-gguf-repository:quantization
LLM_MODEL=openbmb/MiniCPM-V-4.6
LLAMA_CPP_CTX_SIZE=8192
LLAMA_CPP_GPU_LAYERS=999Embedded secrets:
LLAMA_CPP_API_KEY=your-internal-model-key
HF_TOKEN=your-hugging-face-token
APP_SECRET=your-game-session-secretThe Space starts through:
python -m app.runtimeThe Gradio root embeds /game, which serves the custom Phaser app. Static files are under frontend/static.
