WillHbx's picture
update(docs): Update the markdown files
6a2d093
|
Raw
History Blame Contribute Delete
8.24 kB
# CLAUDE.md
## Writing conventions
- Always use regular hyphens (`-`) instead of em dashes (`—`) in all generated text and documentation.
Working guidance for AI pair-programming (Claude Code) on **hackathon-ia-visualnovel** — an
AI-improvised anime visual novel for the Build Small Hackathon. Read this first, then
[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for depth and [`docs/PROMPTS.md`](docs/PROMPTS.md)
for the exact prompts + grammar.
## What this project is (1 paragraph)
A voice-or-text VN where story, NPC dialogue, and art are **generated live** by small local models.
Three model roles: **The Weaver** (director — emits structured JSON directives), **The Voices** (NPC
actor — in-character dialogue), and **The Painter** (SDXL-Turbo — backdrops + sprites). The Weaver and
Voices share **one** LLM (two system prompts). Input is transcribed by **Whisper** (The Ear). Hard
constraint: **≤ 32B total params**, must stay a **Gradio app on a HF Space**.
## The golden rule
**The model proposes, code disposes.** The LLM returns a typed `DirectorOutput`; `state.apply_directives`
is the *only* function that mutates `GameState`. The `.md` files are a *derived view* for humans and for
re-injecting context — never the source of truth. Don't let the LLM hand-edit prose state.
## Commands (uv)
```bash
uv sync # light: gradio + pydantic + pillow (mock mode)
uv run python -m visualnovel.smoke # full loop, no UI, no models
uv run python app.py # custom VN UI → http://localhost:7860
GRADIO_MVP_UI=1 uv run python app.py # gr.Blocks fallback
uv run ruff check . && uv run ruff format .
uv run pytest -q # state round-trip · directives · memory budget
# real backends (install the relevant extras first — see README for ROCm/Metal flags)
VN_MOCK=0 VN_LLM_BACKEND=transformers uv run python app.py
VN_TRACE=runs/$(date +%s).jsonl uv run python app.py # Open-Trace bonus
```
## ⭐ The mock → real migration path (the whole point of this scaffold)
Everything runs **mock-first** (`VN_MOCK=1`). Replace one mock at a time; keep `pytest` + `smoke` green
after each. Work in this order (lowest risk → highest):
1. **LLM — director call.** Implement `llm.LlamaCppLLM` (or `TransformersLLM`), then **delete the
`if config.USE_MOCK:` branches** in `orchestrator.direct_turn` / `init_world` / `compact_memory`
so the real `complete_json(schema=…)` path runs. The JSON schema is already derived from
`schemas.DirectorOutput` via `prompts.directive_schema()`. Tune sampling per `docs/PROMPTS.md §6`.
2. **Painter.** Implement `painter.SdxlTurboPainter._render` (stub already sketches diffusers +
seed + LoRA). Caching, prompt composition, and seed-pinning are done. Set `VN_IMAGE_LORA` to your
fine-tuned anime LoRA when ready.
3. **STT.** `stt.WhisperSTT` is sketched (faster-whisper). The frontend already records audio and
calls `/transcribe`.
4. **Polish.** Sprite moods (pre-gen set, or FLUX.2-Klein conditioning), BiRefNet transparent sprites,
speculative backdrop paint, real `compact_memory` summaries.
Acceptance after each step: `uv run python -m visualnovel.smoke` still completes and `uv run pytest -q`
is green. The mock branches are clearly marked so they're easy to find and remove.
## Architecture at a glance
```
player (text/voice)
│ voice → stt.py (Whisper)
engine.play_turn ─▶ orchestrator.direct_turn ─ one grammar-constrained LLM call ─▶ DirectorOutput
│ (memory.assemble_context builds the bounded prompt)
state.apply_directives (deterministic; the ONLY mutator) ─▶ state.save_memory (.md views)
├─ painter.backdrop / painter.sprite (cached by entity·mood·seed)
engine._view ─▶ ViewState ─▶ frontend/index.html (backdrop + sprite + dialogue)
```
## File responsibilities
| File | Owns | Don't put here |
|---|---|---|
| `visualnovel/schemas.py` | all Pydantic contracts (`GameState`, `DirectorOutput`, `ViewState`) | logic |
| `visualnovel/state.py` | `apply_directives` (sole mutator) + `.md` render | model calls, UI |
| `visualnovel/llm.py` | one LLM wrapper: `complete` / `complete_json(schema)` | game logic |
| `visualnovel/orchestrator.py` | the Weaver: init / direct_turn / compact (+ mock branches) | raw prompt strings (import from `prompts.py`) |
| `visualnovel/characters.py` | the Voices: present-character context | image prompts |
| `visualnovel/painter.py` | prompt compose, cache, seeds, render | story decisions |
| `visualnovel/stt.py` | Whisper transcribe | anything else |
| `visualnovel/memory.py` | bounded context + compaction trigger | persistence format (that's state.py) |
| `visualnovel/engine.py` | façade tying it together → `ViewState` | model internals |
| `app.py` | `gradio.Server` routes, `@app.api`, `@spaces.GPU` | business logic (keep thin) |
| `frontend/index.html` | the entire VN UI; talks via the Gradio JS client | secrets, model logic |
## Conventions
- **Python 3.11+ (3.12 pinned), type hints everywhere, Pydantic for all IO contracts.** `ruff` formats/lints.
- **Keep `app.py` thin.** Logic lives in `visualnovel/` so it's unit-testable without a server.
- **One LLM call per turn by default** (`complete_json` with the directive schema). Extra calls (init,
compaction) are allowed but rare — latency matters.
- **Every Painter call is cached** by `(kind, prompt, seed)`. Generate a sprite **once**; never re-paint
to "refresh" (kills consistency). Seeds are explicit and stored in state.
- **Prompts are versioned in `docs/PROMPTS.md`** (mirrored in `prompts.py`). Don't scatter literals.
- **Heavy imports stay lazy** (inside the real backend classes) so a mock checkout needs nothing.
- **Stream text first, image second.** Dialogue appears before the (slower) image.
## Platform gotchas (read before you fight these)
1. **Python 3.12 is pinned** (`.python-version`) because torch / llama-cpp-python / ctranslate2 ship
wheels for it. Don't bump to a bleeding-edge interpreter unless every dependency has wheels →
otherwise `uv python pin 3.12 && uv sync`.
2. **No CUDA locally — ROCm + Metal.** llama.cpp build flags differ: `-DGGML_METAL=on` (Mac, default),
`-DGGML_HIPBLAS=on` / `-DGGML_HIP=on` (AMD). torch reports **ROCm as "cuda"**`detect_device()`
returns `"cuda"` on the AMD box; `"mps"` on Mac.
3. **CTranslate2 has no Metal/ROCm**`faster-whisper` runs on CPU on both your machines (fine for
short clips). For GPU STT use `mlx-whisper` (Mac) or `whisper.cpp`.
4. **llama.cpp on hosted ZeroGPU is finicky.** Claim the llama.cpp badge **locally**; keep the
`transformers` backend for the Space (`VN_LLM_BACKEND=transformers`).
5. **Image latency is the #1 UX risk.** 1–4-step SDXL-Turbo, 512–768px, text-before-image, cache hard.
6. **Context window is finite** ("Thousand Token Wood"). Feed summary + present sheets + last k turns —
never the whole log. `memory.py` enforces it.
7. **Spaces filesystem is ephemeral.** `.md` memory is per-session unless you wire HF persistent storage.
8. **Qwen3 think-mode on ZeroGPU.** Qwen3-14B via `transformers` sometimes puts the entire JSON answer
inside its `<think>` block and emits nothing after `</think>`. `TransformersLLM.complete_json` handles
this by calling `apply_chat_template(..., enable_thinking=False)` to suppress thinking for structured
output, and falls back to searching inside the think block if no JSON appears after it. Do NOT use
`_quiet_stderr()` around transformers loads — it swallows loading errors that are essential for
ZeroGPU debugging.
## Definition of done (MVP)
A player types (then speaks) a line → an NPC replies in character → the Weaver decides whether the
scene/character changed → the screen shows a coherent backdrop + sprite + dialogue, looping
indefinitely without crashing, on the laptop-safe config, inside the custom VN UI.
## Out of scope (resist these)
Multiplayer; save/load beyond one session; combat/stats UIs; more than ~3 NPCs on screen;
photorealism (anime is cheaper, faster, prettier here); a second LLM just to look "multi-agent".
Ship the loop.