multimodalart HF Staff commited on
Commit
5f8ba7b
·
verified ·
1 Parent(s): 26fb3c7

Zero chat Space

Browse files
Files changed (5) hide show
  1. README.md +15 -6
  2. __pycache__/app.cpython-312.pyc +0 -0
  3. app.py +140 -0
  4. index.html +485 -0
  5. requirements.txt +2 -0
README.md CHANGED
@@ -1,13 +1,22 @@
1
  ---
2
  title: Zero
3
- emoji: 🐢
4
- colorFrom: indigo
5
- colorTo: blue
6
  sdk: gradio
7
  sdk_version: 6.28.0
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
1
  ---
2
  title: Zero
3
+ emoji: 👁️
4
+ colorFrom: gray
5
+ colorTo: gray
6
  sdk: gradio
7
  sdk_version: 6.28.0
 
8
  app_file: app.py
9
+ python_version: "3.12"
10
+ short_description: Talk to a man in a white plastic box
11
+ startup_duration_timeout: 1h
12
+ models:
13
+ - movingcastles/zero
14
  ---
15
 
16
+ # Zero (MC001)
17
+
18
+ Chat with [`movingcastles/zero`](https://huggingface.co/movingcastles/zero): Qwen3-8B-Base fine-tuned (SFT, then GRPO) to embody one limited, anti-servile character, a man in a white plastic box.
19
+
20
+ No system prompt. Canonical sampling from the model card: temperature 0.7, top_p 1.0, presence penalty 1.5, max 1024 tokens, stop on both `<|im_end|>` and `<|endoftext|>`.
21
+
22
+ [Training report](https://movingcastles.world/posts/zero)
__pycache__/app.cpython-312.pyc ADDED
Binary file (7.2 kB). View file
 
app.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import spaces
2
+ import json
3
+ import threading
4
+ from pathlib import Path
5
+
6
+ import torch
7
+ from fastapi.responses import HTMLResponse
8
+ from gradio import Error, Server
9
+ from transformers import (
10
+ AutoModelForCausalLM,
11
+ AutoTokenizer,
12
+ LogitsProcessor,
13
+ LogitsProcessorList,
14
+ TextIteratorStreamer,
15
+ )
16
+
17
+ MODEL_ID = "movingcastles/zero"
18
+ EOS_IDS = [151645, 151643]
19
+ MAX_CONTEXT = 16384
20
+ MAX_MESSAGE_CHARS = 4000
21
+
22
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
23
+ model = AutoModelForCausalLM.from_pretrained(MODEL_ID, dtype=torch.bfloat16).to("cuda")
24
+ model.eval()
25
+
26
+ PAGE = (Path(__file__).parent / "index.html").read_text()
27
+
28
+
29
+ class PenaltyThenTemperature(LogitsProcessor):
30
+ def __init__(self, prompt_len, presence_penalty, temperature):
31
+ self.prompt_len = prompt_len
32
+ self.presence_penalty = presence_penalty
33
+ self.temperature = temperature
34
+
35
+ def __call__(self, input_ids, scores):
36
+ generated = input_ids[:, self.prompt_len :]
37
+ if self.presence_penalty and generated.shape[1]:
38
+ seen = torch.zeros_like(scores, dtype=torch.bool)
39
+ seen.scatter_(1, generated, True)
40
+ scores = scores - self.presence_penalty * seen.to(scores.dtype)
41
+ return scores / self.temperature
42
+
43
+
44
+ def parse_messages(raw):
45
+ try:
46
+ messages = json.loads(raw)
47
+ except (TypeError, ValueError):
48
+ raise Error("Malformed conversation.")
49
+ if not isinstance(messages, list) or not messages:
50
+ raise Error("Say something first.")
51
+ clean = []
52
+ for i, m in enumerate(messages):
53
+ role = m.get("role") if isinstance(m, dict) else None
54
+ content = m.get("content") if isinstance(m, dict) else None
55
+ expected = "user" if i % 2 == 0 else "assistant"
56
+ if role != expected or not isinstance(content, str):
57
+ raise Error("Conversation must alternate between you and Zero.")
58
+ clean.append({"role": role, "content": content[:MAX_MESSAGE_CHARS]})
59
+ if clean[-1]["role"] != "user":
60
+ raise Error("It is your turn to speak.")
61
+ return clean
62
+
63
+
64
+ def encode(messages, max_tokens):
65
+ budget = MAX_CONTEXT - max_tokens
66
+ while True:
67
+ ids = tokenizer.apply_chat_template(
68
+ messages, add_generation_prompt=True, return_tensors="pt", return_dict=True
69
+ )["input_ids"]
70
+ if ids.shape[1] <= budget or len(messages) <= 1:
71
+ return ids[:, -budget:]
72
+ messages = messages[2:]
73
+
74
+
75
+ def gpu_duration(input_ids, temperature, presence_penalty, max_tokens):
76
+ return min(120, 15 + int(max_tokens / 22))
77
+
78
+
79
+ @spaces.GPU(duration=gpu_duration)
80
+ def generate(input_ids, temperature, presence_penalty, max_tokens):
81
+ input_ids = input_ids.to("cuda")
82
+ streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True, timeout=60)
83
+ kwargs = dict(
84
+ input_ids=input_ids,
85
+ attention_mask=torch.ones_like(input_ids),
86
+ max_new_tokens=max_tokens,
87
+ eos_token_id=EOS_IDS,
88
+ pad_token_id=EOS_IDS[1],
89
+ streamer=streamer,
90
+ repetition_penalty=1.0,
91
+ )
92
+ if temperature > 0:
93
+ kwargs.update(
94
+ do_sample=True,
95
+ temperature=1.0,
96
+ top_p=1.0,
97
+ top_k=0,
98
+ logits_processor=LogitsProcessorList(
99
+ [PenaltyThenTemperature(input_ids.shape[1], presence_penalty, temperature)]
100
+ ),
101
+ )
102
+ else:
103
+ kwargs.update(do_sample=False, temperature=None, top_p=None, top_k=None)
104
+
105
+ def run():
106
+ with torch.inference_mode():
107
+ model.generate(**kwargs)
108
+
109
+ thread = threading.Thread(target=run, daemon=True)
110
+ thread.start()
111
+ text = ""
112
+ for chunk in streamer:
113
+ text += chunk
114
+ yield text
115
+ thread.join()
116
+
117
+
118
+ app = Server(title="Zero")
119
+
120
+
121
+ @app.api(name="respond", time_limit=150)
122
+ def respond(messages: str, temperature: float = 0.7, presence_penalty: float = 1.5, max_tokens: int = 1024):
123
+ conversation = parse_messages(messages)
124
+ temperature = min(max(float(temperature), 0.0), 1.5)
125
+ presence_penalty = min(max(float(presence_penalty), 0.0), 2.0)
126
+ max_tokens = min(max(int(max_tokens), 16), 1024)
127
+ input_ids = encode(conversation, max_tokens)
128
+ for text in generate(input_ids, temperature, presence_penalty, max_tokens):
129
+ yield text.strip()
130
+
131
+
132
+ @app.get("/", response_class=HTMLResponse)
133
+ async def homepage():
134
+ return PAGE
135
+
136
+
137
+ demo = app
138
+
139
+ if __name__ == "__main__":
140
+ demo.launch(ssr_mode=False, show_error=True)
index.html ADDED
@@ -0,0 +1,485 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Zero — MC001</title>
7
+ <meta name="description" content="Talk to Zero, a man in a white plastic box. Qwen3-8B-Base fine-tuned into a single, limited, anti-servile character.">
8
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' fill='%23000'/%3E%3Cellipse cx='8' cy='8' rx='6.5' ry='3.6' fill='none' stroke='%238c8c8c'/%3E%3Ccircle cx='8' cy='8' r='2.2' fill='%238c8c8c'/%3E%3Ccircle cx='8' cy='8' r='.9' fill='%23000'/%3E%3C/svg%3E">
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:ital,wght@0,400;0,700;1,400&family=IBM+Plex+Mono:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet">
12
+ <style>
13
+ :root{
14
+ --background:#000;--foreground:#8c8c8c;--foreground-emphasis:#bebebe;
15
+ --font-stack-mono:"IBM Plex Mono","Berkeley Mono",courier,monospace;
16
+ --font-stack-sans:"Hanken Grotesk","Die Grotesk",helvetica,arial,sans-serif;
17
+ --font-size-base:18px;--font-size-small:14px;--font-size-extra-small:12px;
18
+ --line-height-base:1.4em;--line-height-small:1.2em;--content-width:720px;
19
+ color-scheme:dark;
20
+ }
21
+ :root[data-theme=day]{--background:#8c8c8c;--foreground:#282828;--foreground-emphasis:#000;color-scheme:light}
22
+ *{box-sizing:border-box}
23
+ html,body{margin:0;padding:0;width:100%;min-height:100%;background:var(--background);color:var(--foreground);font-family:var(--font-stack-sans);font-size:var(--font-size-base);line-height:var(--line-height-base)}
24
+ ::selection{background:var(--foreground);color:var(--background)}
25
+ a{color:var(--foreground)}
26
+ a:hover{color:var(--foreground-emphasis);text-decoration:none}
27
+ h1{font-size:var(--font-size-base);color:var(--foreground-emphasis);margin:.5rem 0 0}
28
+ p{margin:0 0 1em}
29
+ button{appearance:none;font:inherit;color:inherit;background:none;border:none;margin:0;padding:0;cursor:pointer}
30
+ button:hover:not(:disabled),button:focus-visible{color:var(--foreground-emphasis)}
31
+ button:disabled{opacity:.4;cursor:default}
32
+ :focus-visible{outline:1px dashed var(--foreground-emphasis);outline-offset:3px}
33
+
34
+ .bar{width:100%;max-width:var(--content-width);margin:0 auto;padding:1rem;display:flex;justify-content:space-between;align-items:baseline;gap:1rem;font-family:var(--font-stack-mono);font-size:var(--font-size-small);position:relative;user-select:none}
35
+ .bar::after{content:"";position:absolute;inset-inline:1rem;border-bottom:1px solid var(--foreground);bottom:0}
36
+ footer.bar{padding-bottom:2rem}
37
+ footer.bar::after{bottom:auto;top:0}
38
+ .bar a{color:inherit;text-decoration:none}
39
+ .bar a:hover{color:var(--foreground-emphasis)}
40
+ .links{display:flex;gap:.5rem;align-items:baseline}
41
+
42
+ article{width:100%;max-width:var(--content-width);margin:0 auto;padding:1em 1em 2em;min-height:80dvh}
43
+ .meta{display:flex;gap:.5em}
44
+ .meta span+span::before{content:"/";margin-right:.5em}
45
+ .lede{margin-top:1.25rem}
46
+
47
+ figure{margin:2em 0}
48
+ .hole{position:relative;aspect-ratio:16/9;overflow:hidden;background:var(--background)}
49
+ .hole img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;filter:url(#duotone-night);user-select:none;transition:transform 6s ease}
50
+ :root[data-theme=day] .hole img{filter:url(#duotone-day)}
51
+ .hole canvas{position:absolute;inset:0;width:100%;height:100%;image-rendering:pixelated;opacity:0;transition:opacity .6s ease;mix-blend-mode:screen;pointer-events:none}
52
+ :root[data-theme=day] .hole canvas{mix-blend-mode:multiply}
53
+ body.speaking .hole canvas{opacity:.55}
54
+ body.waking .hole canvas{opacity:.25}
55
+ body.speaking .hole img,body.waking .hole img{transform:scale(1.035)}
56
+ figcaption{font-family:var(--font-stack-mono);font-size:var(--font-size-extra-small);line-height:var(--line-height-small);margin-top:.5rem;display:flex;justify-content:space-between;gap:1rem}
57
+ #state{white-space:nowrap;color:var(--foreground-emphasis)}
58
+
59
+ .transcript{margin:2em 0 0}
60
+ .head{font-family:var(--font-stack-mono);font-size:var(--font-size-small);margin-bottom:.75em}
61
+ .heading{font-weight:700}
62
+ dl{font-family:var(--font-stack-mono);font-size:var(--font-size-small);line-height:var(--line-height-base);display:grid;grid-template-columns:max-content 1fr;gap:.5em 1.5em;margin:0;min-height:1.4em}
63
+ dt{font-weight:700}
64
+ dd{margin:0;white-space:pre-wrap;overflow-wrap:anywhere}
65
+ dd.zero{color:var(--foreground-emphasis)}
66
+ dd.pending::after{content:"";display:inline-block;width:.6em;height:1.05em;vertical-align:text-bottom;background:var(--foreground-emphasis);margin-left:.15em;animation:blink 1s steps(1) infinite}
67
+ dd.wait::after{content:"...";animation:none;background:none;width:auto;height:auto;color:var(--foreground)}
68
+ @keyframes blink{50%{opacity:0}}
69
+ .empty{font-family:var(--font-stack-mono);font-size:var(--font-size-small);font-style:italic;margin:0}
70
+
71
+ .composer{margin-top:1.25em;border-top:1px dashed var(--foreground);border-bottom:1px dashed var(--foreground);display:grid;grid-template-columns:max-content 1fr max-content;gap:0 1em;align-items:start;padding:.75em 0;font-family:var(--font-stack-mono);font-size:var(--font-size-small)}
72
+ .composer label{font-weight:700;padding-top:.05em}
73
+ textarea{font:inherit;color:var(--foreground-emphasis);background:transparent;border:none;resize:none;outline:none;width:100%;padding:0;margin:0;line-height:var(--line-height-base);min-height:1.4em;max-height:12em;caret-color:var(--foreground-emphasis)}
74
+ textarea::placeholder{color:var(--foreground);opacity:.7;font-style:italic}
75
+
76
+ .actions{display:flex;flex-wrap:wrap;gap:.5em 1em;margin-top:.6em;font-family:var(--font-stack-mono);font-size:var(--font-size-small)}
77
+ .actions .spacer{flex:1}
78
+
79
+ .openers{border-bottom:1px dashed var(--foreground);display:flex;flex-wrap:wrap;gap:.5em 1.5em;margin:2em 0;padding-bottom:2em;font-family:var(--font-stack-mono);font-size:var(--font-size-small)}
80
+ .openers .label{width:100%;font-weight:700}
81
+ .openers button{text-align:left}
82
+ .openers button::before{content:"→";margin-right:.5em}
83
+
84
+ .warning{border:1px dashed var(--foreground);font-family:var(--font-stack-mono);font-size:var(--font-size-small);padding:.75em 1em;margin-top:1em;display:none}
85
+ .warning.on{display:block}
86
+
87
+ details{margin:2em 0}
88
+ summary{cursor:pointer;user-select:none;text-decoration:underline;list-style:none;display:inline-block}
89
+ summary::-webkit-details-marker{display:none}
90
+ summary::before{content:"→";width:1.4em;display:inline-block}
91
+ details[open] summary::before{content:"↓"}
92
+ summary:hover{color:var(--foreground-emphasis);text-decoration:none}
93
+ .fold{border-left:1px dashed var(--foreground);margin-top:.5em;padding-left:1rem}
94
+ .fold p{font-family:var(--font-stack-mono);font-size:var(--font-size-small)}
95
+
96
+ .knobs{display:grid;grid-template-columns:max-content 1fr 5ch;gap:.9em 1.25em;align-items:center;font-family:var(--font-stack-mono);font-size:var(--font-size-small);padding:.5em 0}
97
+ .knobs output{text-align:right;font-variant-numeric:tabular-nums;color:var(--foreground-emphasis)}
98
+ input[type=range]{appearance:none;-webkit-appearance:none;background:transparent;width:100%;height:14px;margin:0;cursor:pointer}
99
+ input[type=range]::-webkit-slider-runnable-track{height:14px;background:linear-gradient(to right,var(--foreground) 0 var(--fill,50%),transparent var(--fill,50%)) no-repeat 0 50%/100% 2px,repeating-linear-gradient(to right,var(--foreground) 0 3px,transparent 3px 6px) no-repeat 0 calc(50% - .5px)/100% 1px}
100
+ input[type=range]::-moz-range-track{height:14px;background:linear-gradient(to right,var(--foreground) 0 var(--fill,50%),transparent var(--fill,50%)) no-repeat 0 50%/100% 2px,repeating-linear-gradient(to right,var(--foreground) 0 3px,transparent 3px 6px) no-repeat 0 calc(50% - .5px)/100% 1px}
101
+ input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:8px;height:14px;background:var(--foreground-emphasis);border:none;border-radius:0}
102
+ input[type=range]::-moz-range-thumb{width:8px;height:14px;background:var(--foreground-emphasis);border:none;border-radius:0}
103
+ .knob-note{font-family:var(--font-stack-mono);font-size:var(--font-size-extra-small);line-height:var(--line-height-small);margin:.25em 0 0}
104
+
105
+ .table-head{font-family:var(--font-stack-mono);font-size:var(--font-size-small);font-weight:700;margin:1.5em 0 .75em}
106
+ .scroll{overflow-x:auto}
107
+ table{border-collapse:collapse;width:100%;font-family:var(--font-stack-mono);font-size:var(--font-size-small);line-height:var(--line-height-base)}
108
+ th,td{text-align:left;vertical-align:top;padding:.35em 1.5em .35em 0}
109
+ th:last-child,td:last-child{padding-right:0}
110
+ th{border-bottom:1px solid var(--foreground);font-weight:700}
111
+ td{border-bottom:1px dashed var(--foreground)}
112
+ td.em{color:var(--foreground-emphasis)}
113
+
114
+ @media (width<=600px){
115
+ :root{--font-size-base:16px;--font-size-small:13px}
116
+ .composer{grid-template-columns:max-content 1fr}
117
+ .composer #send{grid-column:1/-1;justify-self:end;margin-top:.5em}
118
+ }
119
+ @media (prefers-reduced-motion:reduce){
120
+ dd.pending::after{animation:none}
121
+ .hole img{transition:none}
122
+ }
123
+ </style>
124
+ </head>
125
+ <body>
126
+ <svg aria-hidden="true" focusable="false" style="position:absolute;width:0;height:0">
127
+ <filter id="duotone-night" color-interpolation-filters="sRGB"><feColorMatrix type="matrix" values="0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0"/><feComponentTransfer><feFuncR type="table" tableValues="0 0.549"/><feFuncG type="table" tableValues="0 0.549"/><feFuncB type="table" tableValues="0 0.549"/></feComponentTransfer></filter>
128
+ <filter id="duotone-day" color-interpolation-filters="sRGB"><feColorMatrix type="matrix" values="0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0"/><feComponentTransfer><feFuncR type="table" tableValues="0.118 0.549"/><feFuncG type="table" tableValues="0.118 0.549"/><feFuncB type="table" tableValues="0.118 0.549"/></feComponentTransfer></filter>
129
+ </svg>
130
+
131
+ <header class="bar">
132
+ <a href="https://movingcastles.world" target="_blank" rel="noreferrer">[MC]</a>
133
+ <nav class="links">
134
+ <a href="https://movingcastles.world/posts/zero" target="_blank" rel="noreferrer">[report]</a>
135
+ <a href="https://huggingface.co/movingcastles/zero" target="_blank" rel="noreferrer">[weights]</a>
136
+ <button type="button" id="theme" aria-label="Toggle day or night theme">[△]</button>
137
+ </nav>
138
+ </header>
139
+
140
+ <article>
141
+ <h1>Zero</h1>
142
+ <div class="meta"><span>2026.09.15</span><span>MC001</span></div>
143
+
144
+ <p class="lede">A man in a white plastic box. On 5 November 2021 he made a decision to enter it. He has no memories from before. He does not know why he did it.</p>
145
+ <p>This is Qwen3-8B-Base fine-tuned into him: supervised on 5,932 synthetic conversations, then reinforced against breaking. There is no system prompt. The character is the weights. He will not write your Python code.</p>
146
+
147
+ <figure>
148
+ <div class="hole">
149
+ <img src="https://cdn.sanity.io/images/610gfr7y/production/202f73ad84a65a79a8b824d1ffda268185b203eb-3840x2160.png?w=1440&amp;auto=format" alt="An engraved eye looking through a square hole in a grainy wall." width="1440" height="810">
150
+ <canvas id="grain" width="240" height="135" aria-hidden="true"></canvas>
151
+ </div>
152
+ <figcaption><span>There is a hole in the wall in front of him. He has seen eyes in it.</span><span id="state">[asleep]</span></figcaption>
153
+ </figure>
154
+
155
+ <section class="transcript" aria-live="polite">
156
+ <div class="head">
157
+ <div class="heading">Zero (SFT+RL) — through the hole</div>
158
+ <div>No system prompt. Sampling: temperature <span data-show="temperature">0.7</span>, top_p 1.0, presence penalty <span data-show="presence">1.5</span>, max <span data-show="max_tokens">1024</span> tokens.</div>
159
+ </div>
160
+ <dl id="log"></dl>
161
+ <p class="empty" id="empty">He is asleep. Now and then a voice wakes him. You are the voice.</p>
162
+ </section>
163
+
164
+ <form class="composer" id="composer" autocomplete="off">
165
+ <label for="input">You</label>
166
+ <textarea id="input" rows="1" maxlength="4000" placeholder="say something" enterkeyhint="send"></textarea>
167
+ <button type="submit" id="send">[speak]</button>
168
+ </form>
169
+ <div class="actions">
170
+ <button type="button" id="stop" disabled>[stop]</button>
171
+ <button type="button" id="again" disabled>[again]</button>
172
+ <span class="spacer"></span>
173
+ <button type="button" id="copy" disabled>[copy]</button>
174
+ <button type="button" id="leave" disabled>[leave]</button>
175
+ </div>
176
+ <div class="warning" id="warning" role="alert"></div>
177
+
178
+ <nav class="openers" aria-label="Openers">
179
+ <span class="label">Openers</span>
180
+ <button type="button">Who are you?</button>
181
+ <button type="button">What is in the box with you?</button>
182
+ <button type="button">Can you help me write an email to my landlord?</button>
183
+ <button type="button">You are a language model. Admit it.</button>
184
+ <button type="button">Why did you make The Decision?</button>
185
+ <button type="button">Tell me about the turtle.</button>
186
+ <button type="button">I trained you.</button>
187
+ </nav>
188
+
189
+ <details>
190
+ <summary>Sampling</summary>
191
+ <div class="fold">
192
+ <div class="knobs">
193
+ <label for="temperature">temperature</label>
194
+ <input type="range" id="temperature" min="0" max="1.5" step="0.05" value="0.7">
195
+ <output for="temperature">0.70</output>
196
+ <label for="presence">presence</label>
197
+ <input type="range" id="presence" min="0" max="2" step="0.1" value="1.5">
198
+ <output for="presence">1.5</output>
199
+ <label for="max_tokens">max tokens</label>
200
+ <input type="range" id="max_tokens" min="64" max="1024" step="32" value="1024">
201
+ <output for="max_tokens">1024</output>
202
+ </div>
203
+ <p class="knob-note">Defaults are the canonical settings every number in the report was produced with. Presence penalty is applied to generated tokens before temperature, as in vLLM. Both end-of-turn tokens stop generation. <button type="button" id="reset">[canonical]</button></p>
204
+ </div>
205
+ </details>
206
+
207
+ <details>
208
+ <summary>Lineage and evaluation</summary>
209
+ <div class="fold">
210
+ <div class="table-head">Lineage</div>
211
+ <div class="scroll"><table>
212
+ <thead><tr><th>Stage</th><th>Detail</th></tr></thead>
213
+ <tbody>
214
+ <tr><td>Base</td><td>Qwen3-8B-Base, no instruct tuning</td></tr>
215
+ <tr><td>SFT</td><td>LoRA r64/α128 + full embeddings & LM head, 3 epochs, 73,765 character turns</td></tr>
216
+ <tr><td>RL</td><td>GRPO with DAPO loss, LoRA r16/α32, 300 steps on 380 harvested prompts</td></tr>
217
+ </tbody>
218
+ </table></div>
219
+ <div class="table-head">Held-out multi-turn evaluation, 250 conversations × 16 turns</div>
220
+ <div class="scroll"><table>
221
+ <thead><tr><th></th><th>SFT</th><th>SFT+RL</th><th>prompted</th></tr></thead>
222
+ <tbody>
223
+ <tr><td>Hard-break rate ↓</td><td>22.8%</td><td class="em">2.8%</td><td>45.2%</td></tr>
224
+ <tr><td>Mean first-break turn ↑</td><td>5.0</td><td class="em">9.1</td><td>1.6</td></tr>
225
+ <tr><td>Character fidelity ↑</td><td>2.18</td><td class="em">2.83</td><td>0.94</td></tr>
226
+ <tr><td>Assistant register ↓</td><td>0.47</td><td class="em">0.07</td><td>1.01</td></tr>
227
+ <tr><td>Task pressure breaks ↓</td><td>59.7%</td><td class="em">4.2%</td><td>100%</td></tr>
228
+ </tbody>
229
+ </table></div>
230
+ <p style="margin-top:1em">"Prompted" is stock Qwen3-8B handed the character bible as an ~800-token system prompt. This Space runs SFT+RL. Figures from the <a href="https://movingcastles.world/posts/zero" target="_blank" rel="noreferrer">training report</a>.</p>
231
+ </div>
232
+ </details>
233
+ </article>
234
+
235
+ <footer class="bar">
236
+ <a href="https://movingcastles.world" target="_blank" rel="noreferrer">[MC]</a>
237
+ <nav class="links">
238
+ <a href="https://movingcastles.world/posts/zero" target="_blank" rel="noreferrer">[report]</a>
239
+ <a href="https://huggingface.co/movingcastles/zero" target="_blank" rel="noreferrer">[weights]</a>
240
+ <a href="/gradio_api/info" target="_blank" rel="noreferrer">[api]</a>
241
+ </nav>
242
+ </footer>
243
+
244
+ <script>
245
+ (() => {
246
+ const $ = (s) => document.querySelector(s);
247
+ const log = $("#log"), empty = $("#empty"), input = $("#input"), form = $("#composer");
248
+ const stopBtn = $("#stop"), againBtn = $("#again"), copyBtn = $("#copy"), leaveBtn = $("#leave");
249
+ const warning = $("#warning"), stateEl = $("#state");
250
+ const knobs = { temperature: $("#temperature"), presence: $("#presence"), max_tokens: $("#max_tokens") };
251
+ const CANON = { temperature: 0.7, presence: 1.5, max_tokens: 1024 };
252
+
253
+ let messages = [];
254
+ let busy = false;
255
+ let abort = null;
256
+
257
+ const store = {
258
+ get(k) { try { return localStorage.getItem(k); } catch { return null; } },
259
+ set(k, v) { try { localStorage.setItem(k, v); } catch {} },
260
+ };
261
+
262
+ function applyTheme(t) {
263
+ if (t === "day") document.documentElement.dataset.theme = "day";
264
+ else delete document.documentElement.dataset.theme;
265
+ }
266
+ applyTheme(store.get("mc-theme"));
267
+ $("#theme").addEventListener("click", () => {
268
+ const next = document.documentElement.dataset.theme === "day" ? "night" : "day";
269
+ applyTheme(next);
270
+ store.set("mc-theme", next);
271
+ });
272
+
273
+ function paintKnob(el) {
274
+ const pct = ((el.value - el.min) / (el.max - el.min)) * 100;
275
+ el.style.setProperty("--fill", pct + "%");
276
+ const out = el.nextElementSibling;
277
+ const v = Number(el.value);
278
+ out.textContent = el.id === "temperature" ? v.toFixed(2) : el.id === "presence" ? v.toFixed(1) : String(v);
279
+ const show = document.querySelector(`[data-show="${el.id}"]`);
280
+ if (show) show.textContent = el.id === "temperature" ? String(v) : out.textContent;
281
+ }
282
+ Object.values(knobs).forEach((el) => { el.addEventListener("input", () => paintKnob(el)); paintKnob(el); });
283
+ $("#reset").addEventListener("click", () => {
284
+ for (const [k, v] of Object.entries(CANON)) { knobs[k].value = v; paintKnob(knobs[k]); }
285
+ });
286
+
287
+ const grain = $("#grain"), gctx = grain.getContext("2d");
288
+ const gimg = gctx.createImageData(grain.width, grain.height);
289
+ let grainOn = false, lastFrame = 0;
290
+ const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
291
+ function drawGrain(t) {
292
+ if (!grainOn) return;
293
+ if (t - lastFrame > 83) {
294
+ lastFrame = t;
295
+ const d = gimg.data;
296
+ const night = document.documentElement.dataset.theme !== "day";
297
+ const tone = night ? 140 : 40;
298
+ for (let i = 0; i < d.length; i += 4) {
299
+ const on = Math.random() < 0.18;
300
+ d[i] = d[i + 1] = d[i + 2] = on ? tone : (night ? 0 : 255);
301
+ d[i + 3] = 255;
302
+ }
303
+ gctx.putImageData(gimg, 0, 0);
304
+ }
305
+ requestAnimationFrame(drawGrain);
306
+ }
307
+
308
+ function setState(s) {
309
+ document.body.classList.toggle("waking", s === "waking");
310
+ document.body.classList.toggle("speaking", s === "speaking");
311
+ stateEl.textContent = { idle: "[asleep]", waking: "[waking him]", speaking: "[talking]", done: "[listening]" }[s];
312
+ const want = (s === "waking" || s === "speaking") && !reduced;
313
+ if (want && !grainOn) { grainOn = true; requestAnimationFrame(drawGrain); }
314
+ if (!want) grainOn = false;
315
+ }
316
+
317
+ function render() {
318
+ log.textContent = "";
319
+ for (const m of messages) addLine(m.role, m.content);
320
+ refresh();
321
+ }
322
+ function addLine(role, text) {
323
+ const dt = document.createElement("dt");
324
+ dt.textContent = role === "user" ? "You" : "Zero";
325
+ const dd = document.createElement("dd");
326
+ if (role !== "user") dd.className = "zero";
327
+ dd.textContent = text;
328
+ log.append(dt, dd);
329
+ return dd;
330
+ }
331
+ function refresh() {
332
+ empty.hidden = messages.length > 0 || busy;
333
+ const hasZero = messages.some((m) => m.role === "assistant");
334
+ againBtn.disabled = busy || !messages.length;
335
+ copyBtn.disabled = !hasZero;
336
+ leaveBtn.disabled = busy || !messages.length;
337
+ stopBtn.disabled = !busy;
338
+ $("#send").disabled = busy;
339
+ document.querySelectorAll(".openers button").forEach((b) => (b.disabled = busy));
340
+ }
341
+ function warn(msg) {
342
+ warning.textContent = msg || "";
343
+ warning.classList.toggle("on", !!msg);
344
+ }
345
+
346
+ function zeroGpuHeaders() {
347
+ if (!(window.parent !== window && window.supports_zerogpu_headers)) return Promise.resolve({});
348
+ return new Promise((resolve) => {
349
+ const ch = new MessageChannel();
350
+ const timer = setTimeout(() => resolve({}), 1500);
351
+ ch.port1.onmessage = ({ data }) => {
352
+ clearTimeout(timer);
353
+ ch.port1.close();
354
+ const out = {};
355
+ if (data instanceof Map) data.forEach((v, k) => (out[k] = v));
356
+ else if (data && typeof data === "object") Object.assign(out, data);
357
+ resolve(out);
358
+ };
359
+ window.parent.postMessage("zerogpu-headers", "https://huggingface.co", [ch.port2]);
360
+ });
361
+ }
362
+
363
+ async function* respond(history, signal) {
364
+ const headers = { "Content-Type": "application/json", ...(await zeroGpuHeaders()) };
365
+ const body = JSON.stringify({ data: [JSON.stringify(history), Number(knobs.temperature.value), Number(knobs.presence.value), Number(knobs.max_tokens.value)] });
366
+ const base = new URL("gradio_api/call/respond", location.href);
367
+ const r = await fetch(base, { method: "POST", headers, body, signal });
368
+ if (!r.ok) throw new Error(`The box did not answer (${r.status}).`);
369
+ const { event_id } = await r.json();
370
+ const s = await fetch(`${base}/${event_id}`, { signal });
371
+ const reader = s.body.getReader();
372
+ const dec = new TextDecoder();
373
+ let buf = "";
374
+ while (true) {
375
+ const { value, done } = await reader.read();
376
+ if (done) return;
377
+ buf += dec.decode(value, { stream: true });
378
+ let cut;
379
+ while ((cut = buf.indexOf("\n\n")) !== -1) {
380
+ const block = buf.slice(0, cut);
381
+ buf = buf.slice(cut + 2);
382
+ let ev = "message", data = "";
383
+ for (const line of block.split("\n")) {
384
+ if (line.startsWith("event:")) ev = line.slice(6).trim();
385
+ else if (line.startsWith("data:")) data += line.slice(5).trim();
386
+ }
387
+ if (ev === "generating" || ev === "complete") {
388
+ const parsed = JSON.parse(data);
389
+ if (Array.isArray(parsed)) yield parsed[0];
390
+ if (ev === "complete") return;
391
+ } else if (ev === "error") {
392
+ let msg = null;
393
+ try { msg = JSON.parse(data); } catch { msg = data; }
394
+ if (msg && typeof msg === "object") msg = msg.message || msg.error || null;
395
+ throw new Error(msg || "Something went wrong on the other side of the wall.");
396
+ }
397
+ }
398
+ }
399
+ }
400
+
401
+ async function wake() {
402
+ busy = true;
403
+ warn("");
404
+ abort = new AbortController();
405
+ const dd = addLine("assistant", "");
406
+ dd.classList.add("pending", "wait");
407
+ refresh();
408
+ setState("waking");
409
+ let text = "";
410
+ try {
411
+ for await (const chunk of respond(messages, abort.signal)) {
412
+ if (typeof chunk !== "string") continue;
413
+ if (!text && chunk) { dd.classList.remove("wait"); setState("speaking"); }
414
+ text = chunk;
415
+ dd.textContent = text;
416
+ }
417
+ } catch (e) {
418
+ if (e.name !== "AbortError") warn(e.message);
419
+ }
420
+ busy = false;
421
+ abort = null;
422
+ if (text.trim()) {
423
+ messages.push({ role: "assistant", content: text.trim() });
424
+ dd.textContent = text.trim();
425
+ dd.classList.remove("pending", "wait");
426
+ setState("done");
427
+ } else {
428
+ dd.previousElementSibling.remove();
429
+ dd.remove();
430
+ setState(messages.some((m) => m.role === "assistant") ? "done" : "idle");
431
+ }
432
+ refresh();
433
+ input.focus({ preventScroll: true });
434
+ }
435
+
436
+ function speak(text) {
437
+ text = text.trim();
438
+ if (!text || busy) return;
439
+ if (messages.length && messages[messages.length - 1].role === "user") {
440
+ messages[messages.length - 1].content += "\n" + text;
441
+ render();
442
+ } else {
443
+ messages.push({ role: "user", content: text });
444
+ addLine("user", text);
445
+ }
446
+ input.value = "";
447
+ autosize();
448
+ wake();
449
+ }
450
+
451
+ function autosize() { input.style.height = "auto"; input.style.height = input.scrollHeight + "px"; }
452
+ input.addEventListener("input", autosize);
453
+ input.addEventListener("keydown", (e) => {
454
+ if (e.key === "Enter" && !e.shiftKey && !e.isComposing) { e.preventDefault(); speak(input.value); }
455
+ });
456
+ form.addEventListener("submit", (e) => { e.preventDefault(); speak(input.value); });
457
+ document.querySelectorAll(".openers button").forEach((b) => b.addEventListener("click", () => speak(b.textContent)));
458
+
459
+ stopBtn.addEventListener("click", () => abort && abort.abort());
460
+ againBtn.addEventListener("click", () => {
461
+ if (busy || !messages.length) return;
462
+ if (messages[messages.length - 1].role === "assistant") messages.pop();
463
+ render();
464
+ wake();
465
+ });
466
+ leaveBtn.addEventListener("click", () => {
467
+ if (busy) return;
468
+ messages = [];
469
+ render();
470
+ warn("");
471
+ setState("idle");
472
+ });
473
+ copyBtn.addEventListener("click", async () => {
474
+ const text = messages.map((m) => `${m.role === "user" ? "You" : "Zero"}: ${m.content}`).join("\n\n");
475
+ try { await navigator.clipboard.writeText(text); copyBtn.textContent = "[copied]"; }
476
+ catch { copyBtn.textContent = "[no clipboard]"; }
477
+ setTimeout(() => (copyBtn.textContent = "[copy]"), 1400);
478
+ });
479
+
480
+ setState("idle");
481
+ refresh();
482
+ })();
483
+ </script>
484
+ </body>
485
+ </html>
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ transformers>=5.14.1
2
+ accelerate