Spaces:
Runtime error
Runtime error
Upload 2 files
Browse files- app.py +387 -0
- requirements.txt +10 -0
app.py
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Thousand Token Wood β powered by NVIDIA Nemotron
|
| 3 |
+
ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 4 |
+
A tiny text adventure narrated by NVIDIA's Nemotron 3 Nano 4B, running fully
|
| 5 |
+
locally. The wood can only remember ~1000 tokens; as you wander, its memory
|
| 6 |
+
fills, and when it overflows the oldest things it knew fall away like leaves
|
| 7 |
+
and the wood quietly rewrites itself. The forgetting is the game. The forgetful
|
| 8 |
+
fox is your companion β exactly the kind of NPC this model was built for.
|
| 9 |
+
|
| 10 |
+
Build Small Hackathon 2026 Β· "An Adventure in Thousand Token Wood".
|
| 11 |
+
Targets: Nemotron GPU prize Β· β€4B tiny-model category Β· Off the Grid Β· llama.cpp.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import re
|
| 15 |
+
import html
|
| 16 |
+
import torch
|
| 17 |
+
import gradio as gr
|
| 18 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 19 |
+
|
| 20 |
+
# ββ Model ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 21 |
+
# NVIDIA Nemotron 3 Nano 4B β edge-ready, agentic, reasoning SLM (~4B params).
|
| 22 |
+
# "nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16" -> default (GPU)
|
| 23 |
+
# "nvidia/NVIDIA-Nemotron-3-Nano-4B-FP8" -> lighter footprint
|
| 24 |
+
# "nvidia/NVIDIA-Nemotron-3-Nano-4B-GGUF" -> llama.cpp (Llama Champion badge)
|
| 25 |
+
# Or the larger sibling, still under the 32B cap:
|
| 26 |
+
# "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16" (30B total / ~3B active)
|
| 27 |
+
MODEL_ID = "nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16"
|
| 28 |
+
|
| 29 |
+
# The whole point of the track: the wood remembers only ~1000 tokens.
|
| 30 |
+
MEMORY_TOKENS = 1000
|
| 31 |
+
|
| 32 |
+
print(f"Waking the wood ({MODEL_ID})...")
|
| 33 |
+
# Nemotron-H is a Mamba-Transformer hybrid and ships custom modelling code.
|
| 34 |
+
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)
|
| 35 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 36 |
+
MODEL_ID, torch_dtype="auto", device_map="auto", trust_remote_code=True
|
| 37 |
+
)
|
| 38 |
+
print("The wood is awake.")
|
| 39 |
+
|
| 40 |
+
# Optional ZeroGPU acceleration on Spaces; harmless no-op locally.
|
| 41 |
+
try:
|
| 42 |
+
import spaces
|
| 43 |
+
gpu = spaces.GPU(duration=120)
|
| 44 |
+
except Exception:
|
| 45 |
+
def gpu(fn):
|
| 46 |
+
return fn
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
# Nemotron is a reasoning model. For a storytelling toy we want prose, not a
|
| 50 |
+
# chain of thought, so we switch reasoning off via the documented system
|
| 51 |
+
# directive and strip any stray <think> trace as a safety net.
|
| 52 |
+
# (Verify the exact toggle phrase on the model card before submitting.)
|
| 53 |
+
REASONING_OFF = "detailed thinking off"
|
| 54 |
+
|
| 55 |
+
PERSONA = (
|
| 56 |
+
"You are the voice of Thousand Token Wood β a small, whimsical, ever-shifting "
|
| 57 |
+
"forest at dusk. Narrate in second person ('you'), present tense. Reply with "
|
| 58 |
+
"2 to 3 short sentences of vivid, sensory, slightly mischievous storytelling: "
|
| 59 |
+
"talking mushrooms, a forgetful fox companion, lanterns that hum, paths that "
|
| 60 |
+
"wander off on their own. Never use lists, headings, asterisks, or bracketed "
|
| 61 |
+
"stage directions. Never break character or mention being an AI or a model. "
|
| 62 |
+
"End most replies with one small, open invitation to act. Keep it gentle and "
|
| 63 |
+
"joyful. The wood has a famously poor memory β lean into wonder, not exposition."
|
| 64 |
+
)
|
| 65 |
+
SYSTEM = REASONING_OFF + "\n\n" + PERSONA
|
| 66 |
+
|
| 67 |
+
QUICK_ACTIONS = [
|
| 68 |
+
"Follow the humming",
|
| 69 |
+
"Greet the fox",
|
| 70 |
+
"Pocket a smooth stone",
|
| 71 |
+
"Climb the lantern tree",
|
| 72 |
+
"Listen for a while",
|
| 73 |
+
"Wander deeper",
|
| 74 |
+
]
|
| 75 |
+
|
| 76 |
+
_THINK = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def clean(text: str) -> str:
|
| 80 |
+
text = _THINK.sub("", text)
|
| 81 |
+
text = text.replace("<think>", "").replace("</think>", "")
|
| 82 |
+
return text.strip()
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
# ββ Memory / context management ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 86 |
+
|
| 87 |
+
def count_tokens(messages):
|
| 88 |
+
ids = tokenizer.apply_chat_template(messages, tokenize=True, add_generation_prompt=True)
|
| 89 |
+
return len(ids)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def build_context(history):
|
| 93 |
+
"""Keep the most recent turns that fit inside MEMORY_TOKENS.
|
| 94 |
+
|
| 95 |
+
Returns (kept_messages, cutoff_index, used_tokens). Entries before the
|
| 96 |
+
cutoff have 'fallen out' of the wood's memory.
|
| 97 |
+
"""
|
| 98 |
+
sys_msgs = [{"role": "system", "content": SYSTEM}]
|
| 99 |
+
kept = []
|
| 100 |
+
cutoff = len(history)
|
| 101 |
+
used = count_tokens(sys_msgs)
|
| 102 |
+
for i in range(len(history) - 1, -1, -1):
|
| 103 |
+
trial = [history[i]] + kept
|
| 104 |
+
t = count_tokens(sys_msgs + trial)
|
| 105 |
+
if t > MEMORY_TOKENS and kept:
|
| 106 |
+
break
|
| 107 |
+
kept = trial
|
| 108 |
+
cutoff = i
|
| 109 |
+
used = t
|
| 110 |
+
return kept, cutoff, used
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
@gpu
|
| 114 |
+
def generate(messages):
|
| 115 |
+
prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
| 116 |
+
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
|
| 117 |
+
with torch.no_grad():
|
| 118 |
+
out = model.generate(
|
| 119 |
+
**inputs,
|
| 120 |
+
max_new_tokens=200,
|
| 121 |
+
do_sample=True,
|
| 122 |
+
temperature=0.9,
|
| 123 |
+
top_p=0.95,
|
| 124 |
+
repetition_penalty=1.1,
|
| 125 |
+
pad_token_id=tokenizer.eos_token_id,
|
| 126 |
+
)
|
| 127 |
+
text = tokenizer.decode(out[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)
|
| 128 |
+
return clean(text)
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
# ββ Rendering ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 132 |
+
|
| 133 |
+
def render_scene(history, cutoff):
|
| 134 |
+
rows = []
|
| 135 |
+
for i, m in enumerate(history):
|
| 136 |
+
forgotten = " forgotten" if i < cutoff else ""
|
| 137 |
+
body = html.escape(m["content"])
|
| 138 |
+
if m["role"] == "user":
|
| 139 |
+
rows.append(f'<div class="line you{forgotten}">› {body}</div>')
|
| 140 |
+
else:
|
| 141 |
+
leaf = '<span class="leaf">🍂</span>' if forgotten else ""
|
| 142 |
+
rows.append(f'<div class="line wood{forgotten}">{leaf}{body}</div>')
|
| 143 |
+
return f'<div class="scene" id="scene">{"".join(rows)}</div>'
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def render_memory(used, fallen):
|
| 147 |
+
pct = min(100, used / MEMORY_TOKENS * 100)
|
| 148 |
+
if fallen > 0:
|
| 149 |
+
plural = "s" if fallen != 1 else ""
|
| 150 |
+
note = f'<div class="forget on">🍂 the wood has let {fallen} thing{plural} drift away</div>'
|
| 151 |
+
leaves = '<div class="leaffall">' + "".join(
|
| 152 |
+
f'<span style="--i:{k}"></span>' for k in range(7)
|
| 153 |
+
) + "</div>"
|
| 154 |
+
else:
|
| 155 |
+
note = '<div class="forget">the wood still remembers everything</div>'
|
| 156 |
+
leaves = ""
|
| 157 |
+
return f"""
|
| 158 |
+
<div class="hud">
|
| 159 |
+
<div class="hud-top"><span>Memory of the Wood</span><span class="hud-num">{used} / {MEMORY_TOKENS}</span></div>
|
| 160 |
+
<div class="hud-track"><div class="hud-fill" style="--w:{pct:.0f}%"></div></div>
|
| 161 |
+
{note}
|
| 162 |
+
{leaves}
|
| 163 |
+
</div>"""
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
# ββ Game loop ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 167 |
+
|
| 168 |
+
def start():
|
| 169 |
+
seed = [
|
| 170 |
+
{"role": "system", "content": SYSTEM},
|
| 171 |
+
{"role": "user", "content": "Begin. Place me at the very edge of the wood at dusk in two short sentences, then invite me to step in."},
|
| 172 |
+
]
|
| 173 |
+
opening = generate(seed)
|
| 174 |
+
history = [{"role": "assistant", "content": opening}]
|
| 175 |
+
kept, cutoff, used = build_context(history)
|
| 176 |
+
return render_scene(history, cutoff), render_memory(used, cutoff), history
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def wander(action, history):
|
| 180 |
+
history = list(history or [])
|
| 181 |
+
action = (action or "").strip()
|
| 182 |
+
if action:
|
| 183 |
+
history.append({"role": "user", "content": action})
|
| 184 |
+
|
| 185 |
+
kept, _, _ = build_context(history)
|
| 186 |
+
reply = generate([{"role": "system", "content": SYSTEM}] + kept)
|
| 187 |
+
history.append({"role": "assistant", "content": reply})
|
| 188 |
+
|
| 189 |
+
_, cutoff, used = build_context(history)
|
| 190 |
+
return render_scene(history, cutoff), render_memory(used, cutoff), history, ""
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
# ββ Look & feel ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 194 |
+
|
| 195 |
+
CSS = """
|
| 196 |
+
@import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,600;9..144,800&family=Cormorant+Garamond:ital,wght@0,400;0,500;1,400&family=IBM+Plex+Mono:wght@400;500&display=swap');
|
| 197 |
+
|
| 198 |
+
*, *::before, *::after { box-sizing: border-box; }
|
| 199 |
+
|
| 200 |
+
:root {
|
| 201 |
+
--night: #0c130e;
|
| 202 |
+
--night-2: #101a13;
|
| 203 |
+
--moss: #6f9c5f;
|
| 204 |
+
--moss-dim:#46603c;
|
| 205 |
+
--amber: #e8a13a;
|
| 206 |
+
--amber-2: #f4c46b;
|
| 207 |
+
--parch: #efe4cb;
|
| 208 |
+
--parch-2: #e6d8b9;
|
| 209 |
+
--ink: #2e2618;
|
| 210 |
+
--ink-dim: #6a5c44;
|
| 211 |
+
--rust: #c2762f;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
.gradio-container {
|
| 215 |
+
max-width: 100% !important;
|
| 216 |
+
background: var(--night) !important;
|
| 217 |
+
font-family: 'Cormorant Garamond', serif !important;
|
| 218 |
+
}
|
| 219 |
+
body {
|
| 220 |
+
background:
|
| 221 |
+
radial-gradient(900px 500px at 50% -6%, rgba(232,161,58,0.16), transparent 62%),
|
| 222 |
+
radial-gradient(700px 500px at 12% 110%, rgba(111,156,95,0.10), transparent 60%),
|
| 223 |
+
var(--night) !important;
|
| 224 |
+
}
|
| 225 |
+
footer { display: none !important; }
|
| 226 |
+
|
| 227 |
+
.gradio-container, .gradio-container p, .gradio-container span, .gradio-container div {
|
| 228 |
+
color: var(--parch);
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
/* ββ Header ββ */
|
| 232 |
+
.wood-head { position: relative; max-width: 880px; margin: 0 auto; padding: 2.6rem 1.4rem 0.4rem; text-align: center; overflow: hidden; }
|
| 233 |
+
.wood-title {
|
| 234 |
+
font-family: 'Fraunces', serif; font-weight: 800; font-size: 3.1rem;
|
| 235 |
+
letter-spacing: -0.02em; line-height: 1; color: var(--parch);
|
| 236 |
+
text-shadow: 0 0 32px rgba(232,161,58,0.22);
|
| 237 |
+
}
|
| 238 |
+
.wood-title em { font-style: italic; color: var(--amber); }
|
| 239 |
+
.wood-sub {
|
| 240 |
+
font-family: 'IBM Plex Mono', monospace; font-size: 0.72rem; letter-spacing: 0.18em;
|
| 241 |
+
text-transform: uppercase; color: var(--moss); margin-top: 0.9rem;
|
| 242 |
+
}
|
| 243 |
+
.wood-blurb { font-size: 1.18rem; color: var(--parch-2); max-width: 560px; margin: 0.7rem auto 0; line-height: 1.45; font-style: italic; }
|
| 244 |
+
|
| 245 |
+
/* gentle ambient drift in the header */
|
| 246 |
+
.wood-head::after {
|
| 247 |
+
content: '\\1F342 \\1F342 \\1F342'; position: absolute; top: -14px; left: 0; right: 0;
|
| 248 |
+
font-size: 0.9rem; letter-spacing: 5rem; opacity: 0.12; pointer-events: none;
|
| 249 |
+
animation: sway 9s ease-in-out infinite;
|
| 250 |
+
}
|
| 251 |
+
@keyframes sway { 0%,100% { transform: translateX(-12px) } 50% { transform: translateX(12px) } }
|
| 252 |
+
|
| 253 |
+
.wrap { max-width: 880px; margin: 0 auto; padding: 1rem 1.4rem 2rem; }
|
| 254 |
+
|
| 255 |
+
/* ββ Scene (parchment) ββ */
|
| 256 |
+
.scene {
|
| 257 |
+
background: linear-gradient(180deg, var(--parch), var(--parch-2));
|
| 258 |
+
border: 1px solid #cdbc97; border-radius: 14px;
|
| 259 |
+
padding: 1.6rem 1.8rem; min-height: 220px; max-height: 460px; overflow-y: auto;
|
| 260 |
+
box-shadow: 0 26px 60px -30px rgba(0,0,0,0.85), inset 0 1px 0 rgba(255,255,255,0.4);
|
| 261 |
+
}
|
| 262 |
+
.line { font-size: 1.3rem; line-height: 1.55; color: var(--ink); margin: 0 0 1rem; transition: opacity 0.6s ease; }
|
| 263 |
+
.line.wood { font-family: 'Cormorant Garamond', serif; }
|
| 264 |
+
.line.you {
|
| 265 |
+
font-family: 'IBM Plex Mono', monospace; font-size: 0.9rem; letter-spacing: 0.02em;
|
| 266 |
+
color: var(--rust); font-weight: 500; margin-bottom: 0.6rem;
|
| 267 |
+
}
|
| 268 |
+
.line.forgotten { opacity: 0.26; font-style: italic; }
|
| 269 |
+
.leaf { margin-right: 0.4rem; filter: saturate(0.7); }
|
| 270 |
+
|
| 271 |
+
/* ββ HUD ββ */
|
| 272 |
+
.hud { margin-top: 1.1rem; }
|
| 273 |
+
.hud-top {
|
| 274 |
+
display: flex; justify-content: space-between; align-items: baseline;
|
| 275 |
+
font-family: 'IBM Plex Mono', monospace; font-size: 0.68rem; letter-spacing: 0.16em;
|
| 276 |
+
text-transform: uppercase; color: var(--moss); margin-bottom: 6px;
|
| 277 |
+
}
|
| 278 |
+
.hud-num { color: var(--amber); }
|
| 279 |
+
.hud-track { height: 7px; background: #0a120c; border: 1px solid #1d2a1f; border-radius: 999px; overflow: hidden; }
|
| 280 |
+
.hud-fill {
|
| 281 |
+
height: 100%; width: var(--w); border-radius: 999px;
|
| 282 |
+
background: linear-gradient(90deg, var(--moss), var(--amber));
|
| 283 |
+
transition: width 0.7s cubic-bezier(0.22,1,0.36,1);
|
| 284 |
+
}
|
| 285 |
+
.forget {
|
| 286 |
+
font-family: 'IBM Plex Mono', monospace; font-size: 0.7rem; letter-spacing: 0.08em;
|
| 287 |
+
color: var(--ink-dim); margin-top: 8px; opacity: 0.7;
|
| 288 |
+
}
|
| 289 |
+
.forget.on { color: var(--amber-2); opacity: 1; }
|
| 290 |
+
|
| 291 |
+
/* falling leaves on overflow */
|
| 292 |
+
.leaffall { position: relative; height: 0; }
|
| 293 |
+
.leaffall span {
|
| 294 |
+
position: absolute; top: -8px; left: calc(var(--i) * 15%);
|
| 295 |
+
width: 9px; height: 9px; background: var(--rust); border-radius: 0 100% 0 100%;
|
| 296 |
+
opacity: 0; animation: fall 2.4s ease-in forwards; animation-delay: calc(var(--i) * 0.12s);
|
| 297 |
+
}
|
| 298 |
+
@keyframes fall {
|
| 299 |
+
0% { opacity: 0; transform: translateY(-6px) rotate(0deg); }
|
| 300 |
+
20% { opacity: 0.9; }
|
| 301 |
+
100% { opacity: 0; transform: translateY(70px) rotate(220deg); }
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
/* ββ Inputs ββ */
|
| 305 |
+
.block, [class*="block"], .form, [class*="form"] { background: transparent !important; border: none !important; box-shadow: none !important; }
|
| 306 |
+
label, .label-wrap span { display: none !important; }
|
| 307 |
+
|
| 308 |
+
textarea, input[type="text"] {
|
| 309 |
+
background: rgba(239,228,203,0.06) !important; border: 1px solid #2a3a2d !important;
|
| 310 |
+
border-radius: 11px !important; color: var(--parch) !important;
|
| 311 |
+
font-family: 'Cormorant Garamond', serif !important; font-size: 1.15rem !important;
|
| 312 |
+
}
|
| 313 |
+
textarea:focus { border-color: var(--amber) !important; outline: none !important; box-shadow: 0 0 0 3px rgba(232,161,58,0.12) !important; }
|
| 314 |
+
|
| 315 |
+
button.primary, button[class*="primary"] {
|
| 316 |
+
background: linear-gradient(135deg, var(--amber), var(--rust)) !important;
|
| 317 |
+
color: #1a1006 !important; border: none !important; border-radius: 11px !important;
|
| 318 |
+
font-family: 'IBM Plex Mono', monospace !important; font-weight: 500 !important;
|
| 319 |
+
letter-spacing: 0.1em !important; text-transform: uppercase !important; font-size: 0.82rem !important;
|
| 320 |
+
padding: 12px 20px !important; box-shadow: 0 10px 26px -12px rgba(232,161,58,0.6) !important;
|
| 321 |
+
transition: transform 0.12s ease !important;
|
| 322 |
+
}
|
| 323 |
+
button.primary:hover { transform: translateY(-2px) !important; }
|
| 324 |
+
|
| 325 |
+
.chip-row { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 0.4rem; }
|
| 326 |
+
button.sm, button:not(.primary) {
|
| 327 |
+
background: rgba(111,156,95,0.08) !important; color: var(--moss) !important;
|
| 328 |
+
border: 1px solid #2a3a2d !important; border-radius: 999px !important;
|
| 329 |
+
font-family: 'IBM Plex Mono', monospace !important; font-size: 0.72rem !important;
|
| 330 |
+
letter-spacing: 0.04em !important; padding: 7px 13px !important; transition: all 0.15s ease !important;
|
| 331 |
+
}
|
| 332 |
+
button.sm:hover, button:not(.primary):hover { color: var(--amber) !important; border-color: var(--amber) !important; }
|
| 333 |
+
|
| 334 |
+
.foot {
|
| 335 |
+
text-align: center; font-family: 'IBM Plex Mono', monospace; font-size: 0.64rem;
|
| 336 |
+
letter-spacing: 0.1em; color: var(--moss-dim); margin: 1.6rem auto 0; padding-bottom: 1.6rem;
|
| 337 |
+
}
|
| 338 |
+
.foot b { color: var(--moss); font-weight: 500; }
|
| 339 |
+
|
| 340 |
+
::-webkit-scrollbar { width: 9px; }
|
| 341 |
+
::-webkit-scrollbar-thumb { background: #cdbc97; border-radius: 999px; }
|
| 342 |
+
::-webkit-scrollbar-track { background: transparent; }
|
| 343 |
+
"""
|
| 344 |
+
|
| 345 |
+
HEADER = """
|
| 346 |
+
<div class="wood-head">
|
| 347 |
+
<div class="wood-title">Thousand Token <em>Wood</em></div>
|
| 348 |
+
<div class="wood-sub">a forest that can only remember a thousand tokens</div>
|
| 349 |
+
<div class="wood-blurb">Wander as long as you like. The wood will, in time, forget you were ever here.</div>
|
| 350 |
+
</div>
|
| 351 |
+
"""
|
| 352 |
+
|
| 353 |
+
FOOTER = """
|
| 354 |
+
<div class="foot">
|
| 355 |
+
runs locally · no cloud · narrated by <b>NVIDIA Nemotron 3 Nano 4B</b>
|
| 356 |
+
· built for the build small hackathon 2026
|
| 357 |
+
</div>
|
| 358 |
+
"""
|
| 359 |
+
|
| 360 |
+
# ββ App ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 361 |
+
|
| 362 |
+
with gr.Blocks(title="Thousand Token Wood", css=CSS, theme=gr.themes.Base()) as demo:
|
| 363 |
+
gr.HTML(HEADER)
|
| 364 |
+
|
| 365 |
+
with gr.Column(elem_classes=["wrap"]):
|
| 366 |
+
scene = gr.HTML('<div class="scene"><div class="line wood">The wood stirs...</div></div>')
|
| 367 |
+
memory = gr.HTML(render_memory(0, 0))
|
| 368 |
+
|
| 369 |
+
with gr.Row():
|
| 370 |
+
action = gr.Textbox(placeholder="What do you do?", scale=5, lines=1, autofocus=True)
|
| 371 |
+
go = gr.Button("Wander", variant="primary", scale=1)
|
| 372 |
+
|
| 373 |
+
with gr.Row(elem_classes=["chip-row"]):
|
| 374 |
+
chips = [gr.Button(a, elem_classes=["sm"]) for a in QUICK_ACTIONS]
|
| 375 |
+
|
| 376 |
+
state = gr.State([])
|
| 377 |
+
gr.HTML(FOOTER)
|
| 378 |
+
|
| 379 |
+
demo.load(fn=start, outputs=[scene, memory, state])
|
| 380 |
+
|
| 381 |
+
go.click(fn=wander, inputs=[action, state], outputs=[scene, memory, state, action])
|
| 382 |
+
action.submit(fn=wander, inputs=[action, state], outputs=[scene, memory, state, action])
|
| 383 |
+
|
| 384 |
+
for label, btn in zip(QUICK_ACTIONS, chips):
|
| 385 |
+
btn.click(fn=wander, inputs=[gr.State(label), state], outputs=[scene, memory, state, action])
|
| 386 |
+
|
| 387 |
+
demo.launch()
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.40.0
|
| 2 |
+
torch>=2.1.0
|
| 3 |
+
transformers>=4.48.0
|
| 4 |
+
accelerate>=0.30.0
|
| 5 |
+
einops>=0.8.0
|
| 6 |
+
spaces>=0.28.0
|
| 7 |
+
# Nemotron 3 Nano (Mamba-Transformer hybrid) may also need, depending on the
|
| 8 |
+
# variant/runtime β follow the exact instructions on the model card:
|
| 9 |
+
# mamba-ssm causal-conv1d (for BF16 transformers on GPU)
|
| 10 |
+
# Alternatively use the -GGUF variant via llama.cpp for the Llama Champion badge.
|