Spaces:
Sleeping
Sleeping
Zero chat Space
Browse files- README.md +15 -6
- __pycache__/app.cpython-312.pyc +0 -0
- app.py +140 -0
- index.html +485 -0
- requirements.txt +2 -0
README.md
CHANGED
|
@@ -1,13 +1,22 @@
|
|
| 1 |
---
|
| 2 |
title: Zero
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.28.0
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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&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
|