Spaces:
Paused
Paused
Commit ·
58b47ef
1
Parent(s): 3f5210c
docs: rewrite AGENTS.md as a current mirror of CLAUDE.md
Browse filesCo-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AGENTS.md
CHANGED
|
@@ -1,160 +1,132 @@
|
|
| 1 |
# Hollow — Agent Instructions
|
| 2 |
|
| 3 |
-
>
|
| 4 |
|
| 5 |
## What this is
|
| 6 |
A horror NPC chatbot for the **Build Small Hackathon** (HF / Gradio). Track: 🍄 An Adventure in Thousand Token Wood. **Deadline: Mon June 15, 2026.**
|
| 7 |
|
| 8 |
-
**Pitch:** A lost child at the edge of
|
| 9 |
|
| 10 |
-
**Spaces (deploy
|
| 11 |
- Personal mirror: https://huggingface.co/spaces/Pabloler21/hollow (remote `space`)
|
| 12 |
-
- **Submission** (must live in org): https://huggingface.co/spaces/build-small-hackathon/hollow (remote `hackathon`) —
|
| 13 |
|
| 14 |
-
##
|
|
|
|
|
|
|
|
|
|
| 15 |
|
|
|
|
| 16 |
```bash
|
| 17 |
-
#
|
| 18 |
-
.venv/Scripts/python -m pytest
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
.venv/Scripts/python -
|
| 22 |
-
|
| 23 |
-
#
|
| 24 |
-
.venv/Scripts/python
|
| 25 |
-
|
| 26 |
-
#
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
# Launch with fast finale for testing a specific ending
|
| 30 |
-
HOLLOW_FAST_FINALE=good .venv/Scripts/python app.py
|
| 31 |
-
# Values: good, loop, bad, 1/neutral (aliases for good/loop)
|
| 32 |
-
|
| 33 |
-
# Deploy (push to both HF Spaces)
|
| 34 |
-
git push space main && git push hackathon main
|
| 35 |
```
|
| 36 |
-
|
| 37 |
-
No linter, formatter, or typechecker is configured. After changes, run `pytest` as the only verification step.
|
| 38 |
|
| 39 |
## Stack (decided — do not reopen)
|
| 40 |
-
- **UI:** Gradio 6.x `gr.Blocks` + `gr.Chatbot`.
|
| 41 |
- **Model:** `Qwen/Qwen3-8B` via `transformers`+`@spaces.GPU(duration=90)` on Space; `qwen3:8b` via Ollama (`localhost:11434`) locally. Switch: `IS_SPACE = bool(os.environ.get("SPACE_ID"))`.
|
| 42 |
-
- **Voice:** Kokoro-82M TTS (`voice.py`
|
| 43 |
-
- **State:** plain dict in `gr.State`
|
| 44 |
-
- **
|
| 45 |
-
- **Pkg manager:** `uv` (`uv pip install`). Space build also needs `packages.txt` (espeak-ng) + `python_version: "3.11"` (Kokoro needs <3.13).
|
| 46 |
-
- **Rejected (don't reopen):** Gemma 4 12B (no 8 GB VRAM fit); **Qwen3.5/3.6 (all multimodal → break the transformers loader; the field uses 1-8B, so 8B stays on-brand)**; LangGraph, Qdrant, cloud APIs, fine-tuning, sponsor-model swap (re-tuning risk for speculative prize).
|
| 47 |
|
| 48 |
-
## Submission
|
| 49 |
-
|
| 50 |
-
- **Pending:** demo video (public, YouTube or uploaded), one social post (LinkedIn) — both linked FROM the README; Field Notes blog post.
|
| 51 |
-
- Models ≤32B. Badges pursued: 🔌 Off-the-Grid, 🎨 Off-Brand, 📓 Field Notes.
|
| 52 |
|
| 53 |
## File structure
|
| 54 |
```
|
| 55 |
-
app.py # thread-cap (OMP/OPENBLAS=4) at top;
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
| 69 |
```
|
| 70 |
|
| 71 |
-
## State shape (gr.State) —
|
| 72 |
```python
|
| 73 |
-
{
|
| 74 |
-
"
|
| 75 |
-
"
|
| 76 |
-
"
|
| 77 |
-
"
|
| 78 |
-
"
|
| 79 |
-
"
|
| 80 |
-
"
|
| 81 |
-
"
|
| 82 |
-
"
|
| 83 |
-
"
|
| 84 |
-
"
|
| 85 |
-
"
|
| 86 |
-
"last_aware_memory": str|None, # B2 — avoid re-surfacing the same memory
|
| 87 |
-
"last_activity": float, # vestigial (client owns idle timing now)
|
| 88 |
-
"idle_count": int, # how many idle nudges fired (tier + bond decay)
|
| 89 |
-
"barren_turns": int, # consecutive turns with no captured memory
|
| 90 |
-
"mode": str, # "tester" | "full" — pacing profile
|
| 91 |
-
}
|
| 92 |
```
|
| 93 |
-
Dev seed: `HOLLOW_FAST_FINALE=good|loop|bad` (`1`/`neutral`
|
| 94 |
-
|
| 95 |
-
## Affinity tiers (
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
| 51–75 | Too Human | fluent, claims memories | sad eyes |
|
| 101 |
-
| 76–100| Almost | possessive | `hollow_almost`, no sway |
|
| 102 |
-
|
| 103 |
-
## Ending matrix (`decide_ending`, checked at turn start, fully scripted, no GPU)
|
| 104 |
-
Mode-driven `PACING` config in `memory.py` (tester = fast/judge-friendly, full = longer experience). Single gate so no lower threshold can race a warm run:
|
| 105 |
- **tester:** `bad` at `tone≤−30 ∧ turn≥4 ∧ wounds≥2`; `good/loop` at `aff≥50 ∧ claimed≥2` branched by `tone≥20`.
|
| 106 |
- **full:** `bad` at `tone≤−30 ∧ turn≥6 ∧ wounds≥2`; `good/loop` at `aff≥68 ∧ claimed≥3` branched by `tone≥20`.
|
| 107 |
-
- **bad** (Wound Loop): recites
|
| 108 |
-
- **good** (Redemption):
|
| 109 |
-
- **loop** (Visitor Loop): fed
|
| 110 |
-
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
#
|
| 118 |
-
|
| 119 |
-
- **
|
| 120 |
-
- **
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
- **
|
| 124 |
-
- **
|
| 125 |
-
- **
|
| 126 |
-
- **
|
| 127 |
-
- **
|
| 128 |
-
- **
|
| 129 |
-
- **
|
| 130 |
-
- **
|
| 131 |
-
- **
|
| 132 |
-
- **
|
| 133 |
-
- **
|
| 134 |
-
- **
|
| 135 |
-
- **
|
| 136 |
-
- **
|
| 137 |
-
- **
|
| 138 |
-
- **
|
| 139 |
-
- **
|
| 140 |
-
- **
|
| 141 |
-
- **
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
## Conventions
|
| 146 |
-
|
| 147 |
- **Everything in English:** code, prompts, UI, dialogue.
|
| 148 |
-
-
|
| 149 |
-
- **
|
| 150 |
-
- **
|
| 151 |
-
-
|
| 152 |
-
- **Art:** grayscale FLUX portraits in `assets/` as base64 WebP. `hollow_rage.webp` is a placeholder copy of `hollow_almost`. To swap a real PNG: `Image.open(png).convert("RGB").resize((512,512),LANCZOS).save(webp,"WEBP",quality=80,method=6)`. Pure asset swap.
|
| 153 |
-
- **Voice:** Kokoro TTS (`af_nicole` voice, 28% fog filter, speed 0.80). CPU only. Import-guarded — app runs silent if kokoro unavailable.
|
| 154 |
-
|
| 155 |
-
## What's next / pending
|
| 156 |
-
- **Current work is on branch `feat/front-door-menu`, branched off `feat/voice-awareness`, NOT deployed.** Both Spaces are frozen at the pre-voice commit (`4deeed5`). Done on the branch (231 tests green): front-door menu (Direction C), Tester/Full pacing modes, How to Play overlay, menu music + mode-select chime→greeting ritual.
|
| 157 |
-
- **Before merge/deploy:** live playthrough in `.venv-tts` + Ollama with the user to verify menu visuals, music, mode-select ritual, mute, and both pacing arcs; then merge `feat/front-door-menu` → `feat/voice-awareness`. Deploy remains **mirror-first**.
|
| 158 |
-
- **README/FIELD_NOTES:** update the stale "no JS" boast; the only JS satisfies browser autoplay-gesture + audio/idle + menu music.
|
| 159 |
-
- **`FIELD_NOTES.md`** and deliverables (demo video, LinkedIn post) are Pablo's.
|
| 160 |
-
- **Mon June 15:** submit early + community vote.
|
|
|
|
| 1 |
# Hollow — Agent Instructions
|
| 2 |
|
| 3 |
+
> Cross-tool mirror of `CLAUDE.md` (same context, practices, hard-won error memories). Keep the two in sync. Record CURRENT STATE + reusable lessons, NOT a day-by-day changelog (git has that). When something changes, edit the relevant line; don't append.
|
| 4 |
|
| 5 |
## What this is
|
| 6 |
A horror NPC chatbot for the **Build Small Hackathon** (HF / Gradio). Track: 🍄 An Adventure in Thousand Token Wood. **Deadline: Mon June 15, 2026.**
|
| 7 |
|
| 8 |
+
**Pitch:** A lost child at the edge of a dead wood has no memories of its own. It asks for yours, stores them in a "treasure", later claims them as its own in the first person (the **recall** — core wow). How you *treat* it branches three endings.
|
| 9 |
|
| 10 |
+
**Spaces (deploy mirror-first, both every time):**
|
| 11 |
- Personal mirror: https://huggingface.co/spaces/Pabloler21/hollow (remote `space`)
|
| 12 |
+
- **Submission** (must live in the org): https://huggingface.co/spaces/build-small-hackathon/hollow (remote `hackathon`) — ZeroGPU `zero-a10g`, public.
|
| 13 |
|
| 14 |
+
## WHERE WE ARE (read first)
|
| 15 |
+
- **Branch `feat/child-redesign`** (off `feat/voice-awareness`), **NOT deployed**. Both Spaces frozen at the pre-voice commit `4deeed5`. **269 tests green.**
|
| 16 |
+
- **Done this redesign:** menu → cinematic **intro** (lore cards) → **game-view rebuilt as a letterboxed foggy scene** (Direction 3b). The child is a **cut-out silhouette** standing in fog (not an arch); corner-drawer collections; conversation as a **fading subtitle transcript** with a labeled two-voice split; **amber = memory / red = cruelty** color language; **whispered in-voice feedback cues**; full **lore** (Caor / the Gaunt / brother Edren); "What it remembers" weaves the memories it stole from you; **O2** scene (edges dissolve to shadow/fog); **F1** stone command-box input; deterministic dev seed; finale pacing/convulse/sigh fixes; an **end screen** (§27 — epitaph + credits over the fog, revealed by an `ended-now` marker).
|
| 17 |
+
- **Next:** (1) live-verify the redesign in `.venv-tts` + Ollama with Pablo; (2) **rework the end screen into layout B** (epitaph as hero, credits demoted to a quiet bottom footer — current §27 layout is unbalanced); (3) replicate the GOOD finale's snappy pacing + **silent convulse beat** to the **loop & bad** finales (only good has it); (4) optional: blur the central frame edges MORE via the scene **mask + lower contrast** (the heavy frame-edge shadow approach FAILED — see gotchas); (5) merge `feat/child-redesign` → `feat/voice-awareness`, then deploy **mirror-first**; (6) deliverables (demo video, LinkedIn, Field Notes) are Pablo's.
|
| 18 |
|
| 19 |
+
## Commands
|
| 20 |
```bash
|
| 21 |
+
# Tests (no model/GPU needed — tests mock or avoid run_turn). 3.13 .venv is silent (fine).
|
| 22 |
+
./.venv/Scripts/python -m pytest -q
|
| 23 |
+
./.venv/Scripts/python -m pytest tests/test_memory.py # one file
|
| 24 |
+
./.venv/Scripts/python -m pytest tests/test_app.py::TestRenderTitle # one class/test
|
| 25 |
+
./.venv/Scripts/python -c "import app; print('ok')" # import smoke check
|
| 26 |
+
|
| 27 |
+
# Local voice playthrough — PowerShell (&& and inline env-prefix don't work); needs Ollama running.
|
| 28 |
+
$env:HOLLOW_FAST_FINALE='good'; ./.venv-tts/Scripts/python app.py # good|loop|bad (1/neutral→good/loop)
|
| 29 |
+
|
| 30 |
+
# Deploy — mirror-first: push space → wait ~3-4 min (Kokoro/spacy build) → Ctrl+F5, verify → then hackathon.
|
| 31 |
+
git push space main # then, only after verifying the mirror:
|
| 32 |
+
git push hackathon main
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
```
|
| 34 |
+
No linter/formatter/typechecker. `pytest` + `import app` are the only automated verification. Visual/audio is **human-only** (Pablo, live).
|
|
|
|
| 35 |
|
| 36 |
## Stack (decided — do not reopen)
|
| 37 |
+
- **UI:** Gradio 6.x `gr.Blocks` + `gr.Chatbot`. Three full-viewport views toggled by visibility: `menu_view` (Fear & Hunger front door, modes Tester/Full + How-to-Play) → `intro_view` (client-driven typewriter lore cards) → `game_view` (the scene). Horror visuals are pure CSS; the `<head>` JS controller (`_HEAD_JS`) owns menu music, the chime→greeting ritual, the intro cards, mute, idle timing, lifting the tone/cue classes onto `#game-view`, and revealing the end screen.
|
| 38 |
- **Model:** `Qwen/Qwen3-8B` via `transformers`+`@spaces.GPU(duration=90)` on Space; `qwen3:8b` via Ollama (`localhost:11434`) locally. Switch: `IS_SPACE = bool(os.environ.get("SPACE_ID"))`.
|
| 39 |
+
- **Voice:** Kokoro-82M TTS (`voice.py`, `af_nicole`, speed 0.94, "28% fog" numpy DSP), **CPU**, import-guarded → **silent if unavailable** (3.13 `.venv` has no kokoro; voice lives in 3.12 `.venv-tts`, and on the Space at py3.11). `_clean_for_tts` strips dashes (Kokoro verbalizes them).
|
| 40 |
+
- **State:** plain dict in `gr.State`. **Parsing:** Pydantic v2 + fallback. **Pkg:** `uv`. Space build needs `packages.txt` (espeak-ng) + `python_version: "3.11"` (Kokoro needs <3.13).
|
| 41 |
+
- **Rejected (don't reopen):** Gemma 4 12B (VRAM); Qwen3.5/3.6 (multimodal breaks the loader); LangGraph, Qdrant, cloud APIs, fine-tuning, sponsor-model swap.
|
|
|
|
|
|
|
| 42 |
|
| 43 |
+
## Submission (Field Guide REQ-06)
|
| 44 |
+
Space in the org + complete README IS the entry (no form). README frontmatter has track + badges + write-up. **Pending:** demo video, one LinkedIn post (both linked FROM README), Field Notes post. Models ≤32B. Badges: 🔌 Off-the-Grid, 🎨 Off-Brand, 📓 Field Notes.
|
|
|
|
|
|
|
| 45 |
|
| 46 |
## File structure
|
| 47 |
```
|
| 48 |
+
app.py # thread-cap (OMP/OPENBLAS/MKL=4) at top; 3 views (menu/intro/game) + end-overlay; chat() generator;
|
| 49 |
+
# _start_turn (stashes msg in state["_pending_msg"], clears box); 3 _play_finale_*; _enter_game/_show_intro;
|
| 50 |
+
# _reset / _to_menu; _render_title(state, show_end) (emits ended-now marker on final finale frame);
|
| 51 |
+
# _voice_html / _voice_and_pause; _recovered(state); _render_bond; _on_idle; _HEAD_JS controller
|
| 52 |
+
voice.py # speak(text)->base64 wav|None — Kokoro whisper + locked DSP, CPU, import-guarded; _clean_for_tts; thread-cap
|
| 53 |
+
engine.py # run_turn + run_turn_stream generators; _EXTRACTION_SYSTEM; IS_SPACE switch; repetition_penalty. Gen+extract in ONE @spaces.GPU
|
| 54 |
+
character.py # build_system_prompt(...); _WORLD note; OWN_FRAGMENTS (5-line Caor arc); OPENING_LINE; INTRO_CARDS/INTRO_SEQUENCES; FRAGMENT_TONES
|
| 55 |
+
memory.py # PACING (tester/full); get_tier; apply_update (+sanitize/Hollow-words filter); should_recall; decide_ending (force_ending bypass); style_signal; pick_aware_memory — pure
|
| 56 |
+
finale.py # finale_steps (loop) / _bad (wound) / _good (redemption) — pure data; good has a SILENT frenzy beat + Edren recovery
|
| 57 |
+
render.py # render_entity(affinity, mode, seq, tone, cue) — emits .tone-now + .cue-now markers; cut-out silhouette + finale modes;
|
| 58 |
+
# render_recovered(fragments_told, claimed) — its arc + the stolen memories; render_treasure(...)
|
| 59 |
+
sting.py # numpy audio synth (sting/chime/heartbeat/sigh/flatline/menu_loop/type_tick) — HF rejects committed .wav
|
| 60 |
+
styles.css # §19-22 finale/progress/typing; §23 menu; §24 how-to; §25 intro; §26 GAME SCENE (3b); §27 END SCREEN
|
| 61 |
+
assets/ # hollow_{base,terror,almost,end,rage,peace}.webp + *_cut.webp (rembg alpha silhouettes); intro_*.webp; background.webp
|
| 62 |
+
tests/ # 269 tests: test_{memory,render,finale,schemas,engine,character,app,sting,voice}.py
|
| 63 |
+
docs/superpowers/{specs,plans}/ # approved specs + plans; AGENT-EXECUTION-BRIEF.md (how to execute a plan here)
|
| 64 |
```
|
| 65 |
|
| 66 |
+
## State shape (gr.State) — read NEW fields with `.get(...)`
|
| 67 |
```python
|
| 68 |
+
{ "affinity": int, # 0..100, start 20 — how MUCH they share (Bond)
|
| 69 |
+
"tone": int, # -100..100, start 0 — how they TREAT it
|
| 70 |
+
"treasure": list[str], "claimed": list[str], # shared / reclaimed (recall dedup)
|
| 71 |
+
"wounds": list[str], # cruel quotes (cap 10), shown redacted ▮
|
| 72 |
+
"history": list[dict], "turn": int, "last_recall_turn": int|None,
|
| 73 |
+
"fragments_told": int, # OWN_FRAGMENTS surfaced (redemption arc)
|
| 74 |
+
"ended": bool, "ending": str|None, # "good"|"loop"|"bad"
|
| 75 |
+
"force_ending": str|None, # dev seed forces this ending (deterministic)
|
| 76 |
+
"msg_lengths": list[int], "last_aware_memory": str|None,
|
| 77 |
+
"last_activity": float, "idle_count": int,
|
| 78 |
+
"greeted": bool, "mode": str, # "tester"|"full" pacing
|
| 79 |
+
"chosen_name": str|None, "named": bool, # the child gropes toward a name
|
| 80 |
+
"_pending_msg": str } # transient — _start_turn stashes the sent msg, chat() pops it
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
```
|
| 82 |
+
Dev seed: `HOLLOW_FAST_FINALE=good|loop|bad` (`1`/`neutral`→good/loop) — seeds state AND `force_ending` so the named ending fires deterministically on the next message.
|
| 83 |
+
|
| 84 |
+
## Affinity tiers (base voice; tone modulates on top)
|
| 85 |
+
0–25 Hollow (broken, barely speaks) · 26–50 Curious (repeats you) · 51–75 Too Human (claims memories) · 76–100 Almost (possessive, `almost` face, no sway).
|
| 86 |
+
|
| 87 |
+
## Endings (`decide_ending`, turn start, fully scripted, no GPU)
|
| 88 |
+
`force_ending` (dev seed) wins first. Else, one tone-branched gate (no lower threshold races a warm run):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
- **tester:** `bad` at `tone≤−30 ∧ turn≥4 ∧ wounds≥2`; `good/loop` at `aff≥50 ∧ claimed≥2` branched by `tone≥20`.
|
| 90 |
- **full:** `bad` at `tone≤−30 ∧ turn≥6 ∧ wounds≥2`; `good/loop` at `aff≥68 ∧ claimed≥3` branched by `tone≥20`.
|
| 91 |
+
- **bad** (Wound Loop): recites cruelties (redacted→un-redacted), frenzy + triple full-screen stab + scream, reveals rage face, "see you... soon."
|
| 92 |
+
- **good** (Redemption): warmth surfaced its real past; confesses (Caor/the Gaunt), recovers brother **Edren**, RETURNS the treasure (un-claimed, lit), first smile (`peace`→`peace_dissolve`), resolves in peace — NO sequel/quest hook.
|
| 93 |
+
- **loop** (Visitor Loop): fed but never loved → stays a predator, consumes you, ends with YOU speaking its opening line.
|
| 94 |
+
- The finale's final frame emits an `ended-now[data-ending]` marker → `_HEAD_JS applyEnd()` reveals the **end screen** (§27) with the per-ending epitaph.
|
| 95 |
+
- Prompt tone bands (separate from routing): warm ≥15 / wounded −22..−1 / hostile ≤−22 (injects wounds). Mimicry always on.
|
| 96 |
+
|
| 97 |
+
## Lore (canon)
|
| 98 |
+
The child is a dead tithe of **Caor**, a salt-fen hamlet that, in famine, gave **one** child to **the Gaunt** (the old hunger breathing in the wood) to spare the rest. Its family paid the debt; reed-masked bearers bound it at the treeline and left; it starved unburied and became the Gaunt's hook — the thing that waits at the edge and begs memories to fill the hollow where its own used to be. It had a **brother (Edren)** who braided its hair. Recovered as the 5-line `OWN_FRAGMENTS` arc; `_WORLD` note in the system prompt keeps improvised dialogue consistent.
|
| 99 |
+
|
| 100 |
+
## Game-view scene anatomy (§26) + end screen (§27)
|
| 101 |
+
- `#game-view` is `position:fixed; inset:0` and defines the **frame geometry as CSS vars** (`--fw/--fh/--band-v/--band-h`). Every in-frame region (drawers, `#game-entity`, `#game-dialogue` subtitle, `#game-groundfog`) is `position:fixed` and anchored to those vars so it sits INSIDE the centered 16:9 frame. `#game-inputbar` is the stone command box at the viewport base, below the frame.
|
| 102 |
+
- Layers: `#game-bg` (blurred forest) → `#game-scene` (sharp forest, radial-mask feathered = O2) → `#game-vig`/`#game-frame-edge` → `#game-entity` silhouette → `#game-groundfog`.
|
| 103 |
+
- **End screen (§27):** `#end-overlay` is a top-level `gr.Column` (`position:fixed; inset:0`), always mounted, `opacity:0/pointer-events:none` until JS adds `.shown`. Epitaph text set per ending by `applyEnd()`; two real `gr.Button`s wired to `_reset` (begin again) and `_to_menu` (leave the wood).
|
| 104 |
+
- **Voice/Presence:** every reply + finale line is spoken (Kokoro). Reply text streams; each finished sentence → a **client-side audio queue** in `_HEAD_JS` (sentences never cut). **Mute is client-side** (the controller owns `{muted}` in localStorage; the server always synthesizes). Awareness: B3 `style_signal`, B2 `pick_aware_memory` (every 3rd non-recall turn), B1 idle (client-timed 60 s → hidden `#idle-trigger` → `_on_idle`, escalating, −3 aff/−2 tone), B4 `_memory_plea_level` (barren turns plead harder). Feedback cues: capture→Treasure chip pulse + "— it keeps this —"; recall→amber line + claimed mark; recover→"— it remembers —".
|
| 105 |
+
|
| 106 |
+
## Gotchas (DON'T re-learn the hard way)
|
| 107 |
+
- **`position:fixed` is the layout primitive here** (menu/intro/game/end). A `transform`/`filter` on ANY ancestor traps fixed children → they fall into Gradio flow. `#game-stage` has `transform:none`; `#game-entity` centers via `left:calc(50% - var(--fw)*0.15)` (NO transform) so the full-screen `.entity-ghost` finale stab isn't trapped in the narrow column.
|
| 108 |
+
- **Gradio wraps each component (`gr.HTML`, `gr.Row`, `gr.Button`) in a full-width `.block`.** So `align-items:center` on a parent flex centers the *block*, not your inner content, and a `gr.Row` of buttons defaults to spreading them. To truly center an overlay's content, neutralize the wrappers (`width`/`max-width`/`background`/`border`) and override the row/buttons explicitly.
|
| 109 |
+
- **Don't dissolve scene edges with a heavy full-viewport inset shadow** — it darkens the viewport border and leaves a bright-center RECTANGLE (the "cut"). Dissolve via the `#game-scene` radial **mask + low brightness contrast** vs the blur. (210px/90px frame-edge is the accepted ceiling; 250/120 broke it.)
|
| 110 |
+
- **Verify CSS against the RUNNING app, never the static harness.** Headless screenshots are fragile in this shell; `.venv` for fast layout probes, `.venv-tts` + Pablo's eyes/ear for the real playthrough. Audio can only be confirmed by ear. **Do NOT launch/headless-verify as an agent — that step is human-only.**
|
| 111 |
+
- **Gradio replaces the DOM each update:** CSS *animations* restart (the flash/screamer mechanism); *transitions* don't animate across updates. `prefers-reduced-motion`: disable sway/fog only, NEVER opacity (killed the terror flash once).
|
| 112 |
+
- **Cut-out silhouettes:** `hollow_*_cut.webp` (rembg alpha). Child materializes via brightness/opacity + a bottom fade mask into the fog — NO `blur` (reads as broken image), NO `scale`. Finale/flash overlays (`.entity-flick` etc.) MUST use `object-fit:contain`+bottom like the base, or they balloon past the frame.
|
| 113 |
+
- **Finale audio:** one-shot cues (sigh/flatline/sting) carry `class="cue-audio"` (NOT autoplay) → routed through the client voice queue so they don't talk over the playing line; the looping heartbeat stays autoplay. `_voice_and_pause` paces spoken steps to `audio_len + 0.1` (silent beats keep their scripted pause). Good finale splits the convulsion into a spoken line + a **silent `frenzy` beat** so the queue drains and the relief sigh lands exactly on the smile.
|
| 114 |
+
- **Input clears on send via state stash:** `_start_turn` sets `state["_pending_msg"]` + clears the box; `chat()` pops it. Do NOT recover from chatbot history — Gradio normalizes message `content` to a list (`'list' has no attribute 'strip'`).
|
| 115 |
+
- **The opening always renders the base face** (`_enter_game` → `render_entity(20)`), never the seeded Almost tier — it's the vulnerable first contact.
|
| 116 |
+
- **Marker pattern for client state:** `render_entity`/`_render_title` emit hidden `.tone-now[data-tone]` / `.cue-now[data-cue][data-seq]` / `.ended-now[data-ending]` spans; `applyTone()`/`applyCue()`/`applyEnd()` (polled in `_HEAD_JS wire()`) lift them onto `#game-view` or reveal the overlay. Markers are siblings of `#game-bg`, not ancestors — that's why the JS lifts them. Reuse `openingRitual()` etc.; don't redefine existing JS functions. Don't add outputs to the finale generator tuples (tuple-desync hazard) — use the marker.
|
| 117 |
+
- **Qwen emits `+2` (invalid JSON)** without positive anchors → bond froze. Sanitizer in `apply_update` + "no leading +" + dual examples. Keep both.
|
| 118 |
+
- **Recall pollution:** extraction grabbed Hollow's own recalled line as a "new memory". `_is_hollows_words` (>60% overlap dropped) + `repetition_penalty 1.3`. Keep both.
|
| 119 |
+
- **One `@spaces.GPU` per turn** (gen+extract). NO threading around `run_turn` (breaks ZeroGPU); a `TextIteratorStreamer` thread INSIDE the GPU call is the supported stream pattern (verify on the Space; fall back to non-stream if it breaks).
|
| 120 |
+
- **transformers 5.x:** `apply_chat_template(...,return_tensors="pt")`→`BatchEncoding` (`hasattr(t,"input_ids")`); `dtype=` not `torch_dtype`; thinking OFF (`enable_thinking=False` / `"think":False`).
|
| 121 |
+
- **HF rejects committed binaries (.wav):** synth at runtime (`sting.py`). New non-image binaries: same.
|
| 122 |
+
- **Don't edit `.py`/`.css`/`.md` via PowerShell string ops** — mangles UTF-8 (em-dashes, `❯`, accents, emoji). Use the Edit tool.
|
| 123 |
+
- **Cap CPU math threads before torch loads** (`OMP/OPENBLAS/MKL=4`) or OpenBLAS exhausts the CPU allocator → Kokoro silently disabled (NOT real RAM exhaustion).
|
| 124 |
+
- **Backend safety net:** `run_turn` failures → in-character "fog swallowed your words" reply. Msgs >500 chars declined in character. **Ollama 500 = ROCm `cudaMalloc` corruption** (every reply becomes the fallback) → restart Ollama; the Space is immune.
|
| 125 |
+
- **Gradio 6 moved `js=`/`head=` to `launch()`.** Inject JS via `launch(head=...)`. `gr.HTML` initial values are client-rendered (in `/config`, not page HTML).
|
| 126 |
+
|
| 127 |
+
## Working style & conventions
|
|
|
|
|
|
|
|
|
|
| 128 |
- **Everything in English:** code, prompts, UI, dialogue.
|
| 129 |
+
- Phase by phase; explain *why* before code; no new deps/complexity without asking; push back on scope creep.
|
| 130 |
+
- Big changes flow **spec → plan (writing-plans) → execution**. Opus writes specs/plans; the executor implements task-by-task (read `docs/superpowers/AGENT-EXECUTION-BRIEF.md` before executing a plan).
|
| 131 |
+
- **One task/phase per commit**, exact message from the plan + trailer: `Co-Authored-By: Claude <model> <noreply@anthropic.com>` (the model you actually are).
|
| 132 |
+
- Keep the test suite green (`pytest -q`); `import app` must stay OK after every change.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|