Spaces:
Running on Zero
Running on Zero
| title: Ephemeral Hearts | |
| emoji: π | |
| colorFrom: green | |
| colorTo: indigo | |
| sdk: gradio | |
| sdk_version: 6.18.0 | |
| python_version: '3.12' | |
| app_file: app.py | |
| pinned: false | |
| tags: | |
| - track:wood | |
| - sponsor:modal | |
| - achievement:offgrid | |
| - achievement:offbrand | |
| - achievement:llama | |
| - achievement:sharing | |
| - achievement:fieldnotes | |
| license: apache-2.0 | |
| thumbnail: >- | |
| https://cdn-uploads.huggingface.co/production/uploads/65c10674c4dcb96c31dd84c9/aWVwpGKH963R2e81blgxO.png | |
| ## Team | |
| - WillHbx β https://huggingface.co/WillHbx | |
| - SuperPoivre13 β https://huggingface.co/SuperPoivre13 | |
| # hackathon-ia-visualnovel π | |
| > **Thousand Token Wood** β an AI-improvised, voice-or-text **anime visual novel**. | |
| > Build Small Hackathon (Chapter Two). Story, characters, and art are generated **live** by | |
| > small local models β nothing is pre-scripted. | |
| You step into a wood that is being *dreamed into existence* around you. Every spirit you meet is | |
| conjured on the spot; every backdrop is painted the moment you arrive. Because the wood is dreamed | |
| by a small, slightly forgetful mind, it is whimsical and never the same twice β **the model's | |
| quirks aren't bugs, they're the wood dreaming.** Take the models out and there's no game, only an | |
| empty Gradio shell. That's what makes the AI *load-bearing*. | |
| --- | |
| ## π Build Small Hackathon submission | |
| **Ephemeral Hearts** is an AI-improvised anime dating visual novel: the story, every character, | |
| every line of dialogue, every backdrop and sprite, and even the character voices are generated | |
| **live** by small local models. Nothing is pre-scripted. Five AI roles collaborate in **under 18B | |
| parameters total** behind one golden rule, *the model proposes, code disposes*: the LLM returns a | |
| typed `DirectorOutput` (one grammar-constrained call per turn) and deterministic code is the **only** | |
| thing that mutates game state. The same `visualnovel/` package runs in three places unchanged, | |
| selected by env vars: fully offline on a laptop (`llama.cpp` + `diffusers`), on on-demand Modal | |
| GPUs, or on a ZeroGPU Space. | |
| **Tech:** Qwen3-14B (the Weaver + the Voices, shared weights) Β· SDXL + a fine-tuned anime LoRA (the | |
| Painter) Β· Whisper (the Ear) Β· Kokoro-82M (the Voice) Β· Gradio `gradio.Server` with a bespoke | |
| HTML/JS frontend Β· Pydantic-derived JSON grammar Β· `uv`. | |
| - π₯ **Demo video:** https://youtu.be/NVDTg_oMUY4 | |
| - π **Blog / field notes:** https://huggingface.co/blog/WillHbx/ephemeral-hearts | |
| - π¬ **Social post:** https://www.linkedin.com/posts/lorenzo-lepoivre_ai-generativeai-llm-share-7471625802258939904-Pjx-/ | |
| **Track:** Thousand Token Wood | |
| **Sub-tracks:** Off-Brand Β· Best Demo Β· Best Agent Β· Bonus Quest Champion Β· Judges' Wildcard | |
| **Badges:** π Off the Grid Β· π¨ Off-Brand (Custom UI) Β· π¦ Llama Champion (llama.cpp) Β· | |
| π‘ Sharing is Caring (Open-Trace) Β· π Field Notes | |
| --- | |
| ## β¨ This repo runs on a fresh checkout β with **zero models** | |
| It ships in **MOCK mode** (`VN_MOCK=1`, the default): deterministic fake LLM / painter / STT so the | |
| **entire loop works offline** with placeholder art. Open it, watch a turn happen, then have Claude | |
| Code replace each mock with a real model, one module at a time. See [`CLAUDE.md`](CLAUDE.md). | |
| ```bash | |
| uv sync # installs only gradio + pydantic + pillow (seconds) | |
| uv run python -m visualnovel.smoke # full loop in the terminal, no UI, no models | |
| uv run python app.py # custom VN UI β http://localhost:7860 | |
| GRADIO_MVP_UI=1 uv run python app.py # plain gr.Blocks fallback (Phase 0/1) | |
| ``` | |
| --- | |
| ## The cast of models (the βseveral AIsβ) | |
| Four *roles*, four *real* model families (text, image, speech-in, speech-out). The two text roles | |
| **share one LLM** (same weights, two system prompts) to respect the parameter budget. | |
| | In-world name | Role | Backed by | Output | | |
| |---|---|---|---| | |
| | π§΅ **The Weaver** | director / GM β builds the world, keeps canon, decides what changes | LLM (shared) | structured **directives** (JSON) | | |
| | π **The Voices** | actor β speaks as the present spirits, in voice | LLM (shared) | in-character **dialogue** + emotion | | |
| | π¨ **The Painter** | renders backdrops + character sprites | **SDXL-Turbo** (+ your anime-style LoRA) | **images** | | |
| | π **The Ear** | turns spoken words into text | Whisper | transcribed input | | |
| | π£οΈ **The Voice** | speaks the spirits' dialogue aloud (per-character voice) | **Kokoro-82M** (ONNX) | synthesized **audio** | | |
| The Weaver and Voices are one **grammar-constrained LLM call per turn** β `{ speaker, dialogue, | |
| emotion, directives }`. Code applies the directives deterministically; **the LLM never edits state | |
| directly.** | |
| --- | |
| ## Parameter budget β the βbeefyβ config (β€ 32B total) | |
| | Component | Model | ~Params | | |
| |---|---|---| | |
| | LLM (shared) | **Qwen3-14B** | ~14B | | |
| | Image | **SDXL-Turbo** (+ fine-tuned anime LoRA) | ~3.5B | | |
| | STT | **Whisper-large-v3-turbo** | ~0.8B | | |
| | TTS | **Kokoro-82M** (ONNX) | ~0.08B | | |
| | Matting (optional) | BiRefNet | ~0.2B | | |
| | **Total** | | **β 18.6B** β | | |
| MoE models count by **total** params. Everything is hosted on HF (you have credits), so the only | |
| hard limit is β€ 32B. Override any model via env vars in [`visualnovel/config.py`](visualnovel/config.py). | |
| --- | |
| ## Setup & install (uv) | |
| > **Python 3.12** is pinned in `.python-version` β the ML stack (`torch` / | |
| > `llama-cpp-python` / `ctranslate2`) ships wheels for it. The package code is 3.11+ clean, | |
| > so a newer interpreter works too if every dependency publishes wheels for it. | |
| Your hardware is **AMD ROCm + Apple Metal (no local CUDA)**, which changes the GPU build flags: | |
| ```bash | |
| # ---- Apple Silicon (M3 Max) β Metal ---- | |
| uv sync --extra image --extra stt | |
| CMAKE_ARGS="-DGGML_METAL=on" uv pip install llama-cpp-python # Metal is default on macOS | |
| # torch uses the MPS backend automatically. faster-whisper runs on CPU (CTranslate2 has no | |
| # Metal); for GPU STT on Mac use mlx-whisper or whisper.cpp instead. | |
| # ---- AMD RX 7900 XTX β ROCm (Linux) ---- | |
| uv pip install torch --index-url https://download.pytorch.org/whl/rocm6.2 # ROCm torch wheel | |
| CMAKE_ARGS="-DGGML_HIPBLAS=on" uv pip install llama-cpp-python # newer builds: -DGGML_HIP=on | |
| uv sync --extra image --extra stt | |
| # torch reports ROCm as "cuda" (so config.detect_device() returns "cuda" on this box). | |
| # CTranslate2 has no ROCm β faster-whisper runs on CPU (fine for short clips), or use whisper.cpp. | |
| ``` | |
| Then download weights (or let them fetch lazily on first real use) and flip off mock mode: | |
| ```bash | |
| uv run python scripts/download_models.py | |
| VN_MOCK=0 uv run python app.py | |
| ``` | |
| --- | |
| ## How a turn works | |
| ``` | |
| START ββΆ Weaver: dream world + opening scene + first spirit ββΆ Painter: backdrop + sprite | |
| β | |
| βββΆ LOOP | |
| 1. player speaks/types ββΆ Ear (Whisper) ββΆ text | |
| 2. ONE grammar-constrained LLM call ββΆ { speaker, dialogue, emotion, directives } | |
| 3. state.apply_directives(...) deterministically (move scene, add/exit spirit, mood, | |
| relationship, flags, beat, ending) ββΆ .md dream-memory updated | |
| 4. if scene/character changed ββΆ Painter (cached, seed-pinned) | |
| 5. render: backdrop + sprite(mood) + dialogue (text streams first; image fills in) | |
| 6. The Voice (Kokoro) speaks the spirit's line in its per-character voice (cached) | |
| ``` | |
| Deep design in [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md); prompts + JSON schema + GBNF grammar | |
| in [`docs/PROMPTS.md`](docs/PROMPTS.md). | |
| --- | |
| ## Project structure | |
| ``` | |
| hackathon-ia-visualnovel/ | |
| βββ app.py # gradio.Server entry + gr.Blocks MVP fallback (thin) | |
| βββ pyproject.toml # uv project; heavy ML libs are optional extras | |
| βββ .python-version # 3.12 (ML stack ships wheels for it) | |
| βββ frontend/index.html # custom layered VN UI (backdrop Β· sprite Β· dialogue Β· mic) | |
| βββ visualnovel/ | |
| β βββ schemas.py # Pydantic: GameState (truth) + DirectorOutput (LLM contract) | |
| β βββ config.py # paths, flags, model registry, THEMES, device detection | |
| β βββ prompts.py # system prompts + schema helpers (no scattered literals) | |
| β βββ llm.py # MockLLM Β· LlamaCppLLM Β· TransformersLLM behind one interface | |
| β βββ orchestrator.py # the Weaver: init_world / direct_turn / compact_memory | |
| β βββ characters.py # the Voices: present-character actor context | |
| β βββ painter.py # the Painter: prompt compose, cache, MockPainter + SDXL stub | |
| β βββ stt.py # the Ear: Whisper (mock + faster-whisper) | |
| β βββ tts.py # the Voice: per-character speech (mock + Kokoro-82M ONNX) | |
| β βββ state.py # apply_directives (the ONLY mutator) + .md render | |
| β βββ memory.py # bounded context assembly + compaction trigger | |
| β βββ engine.py # faΓ§ade: start_text/start_images Β· play_turn(_text/_images) β ViewState | |
| β βββ utils.py # shared helpers (think-strip, JSON repair, quiet stderr) | |
| β βββ metrics.py # debug-mode metrics + live monitor (no-op in mock/prod) | |
| β βββ trace.py # per-turn JSONL trace (Open-Trace bonus) | |
| β βββ smoke.py # `python -m visualnovel.smoke` | |
| βββ templates/ # the .md dream-memory views (rendered each turn) | |
| βββ tests/ # state round-trip Β· directive apply Β· memory budget (mock, green) | |
| βββ scripts/download_models.py | |
| βββ docs/ # ARCHITECTURE.md Β· PROMPTS.md | |
| ``` | |
| --- | |
| ## Bonus quests targeted | |
| | Badge | How | | |
| |---|---| | |
| | π¨ **Off-Brand** (custom UI) | bespoke VN frontend via `gradio.Server` (`frontend/index.html`) | | |
| | π¦ **Llama-Champion** | LLM through `llama-cpp-python` + JSON-schema/GBNF grammar (run **locally** for the video) | | |
| | π **Off the Grid** | all weights local; demoed with the network off | | |
| | π― **Well-Tuned** | the **fine-tuned SDXL-Turbo anime LoRA**, published on the Hub (`VN_IMAGE_LORA`) | | |
| | π‘ **Open-Trace** | `VN_TRACE=runs/run.jsonl` records every orchestration step β publish as a dataset | | |
| | π **Field-Notes** | write up the diegetic conceit + one-call director pattern | | |
| > β οΈ **llama.cpp on the *hosted* ZeroGPU Space is the trickiest bit** β claim it **locally** in the | |
| > demo video and keep the `transformers` backend (`VN_LLM_BACKEND=transformers`) for the Space. | |
| > See [`docs/ARCHITECTURE.md` Β§7](docs/ARCHITECTURE.md). | |
| --- | |
| ## Quality | |
| ```bash | |
| uv run ruff check . && uv run ruff format . | |
| uv run pytest -q | |
| ``` | |
| ## Submission checklist | |
| - [ ] Registered on the Build-Small org by **June 3** + joined the Gradio Discord. | |
| - [ ] Space is live and loads without errors. | |
| - [ ] Total params **β€ 32B** (stated). | |
| - [ ] It's a Gradio app (`gradio.Server` counts). | |
| - [ ] Demo video + social post. | |
| Built for the **Build Small Hackathon** (Gradio Γ Hugging Face, June 2026). Code Apache-2.0 β match | |
| the licenses of the weights you ship (SDXL-Turbo and Qwen3 are permissive; **check FLUX.2** if you | |
| swap the painter). |