cyberconnectbel commited on
Commit
62070e8
Β·
verified Β·
1 Parent(s): 9c5422e

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +387 -0
  2. 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}">&rsaquo;&nbsp;{body}</div>')
140
+ else:
141
+ leaf = '<span class="leaf">&#127810;</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">&#127810; 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}&thinsp;/&thinsp;{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&nbsp;Token&nbsp;<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 &middot; no cloud &middot; narrated by <b>NVIDIA Nemotron 3 Nano 4B</b>
356
+ &middot; 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.