Custom gr.Server frontend + 6-teacher model (Nemotron), token streaming, arena
Browse files- README.md +18 -6
- _boot.py +136 -0
- _fig.py +1 -1
- _glb.py +1 -0
- _html.py +15 -13
- _probe.py +253 -13
- _three.py +1 -1
- app.py +124 -59
- frontend/app.js +388 -0
- frontend/index.html +507 -0
- requirements.txt +5 -1
- server_app.py +84 -0
README.md
CHANGED
|
@@ -4,18 +4,30 @@ emoji: 🧬
|
|
| 4 |
colorFrom: indigo
|
| 5 |
colorTo: purple
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: "
|
| 8 |
-
app_file:
|
| 9 |
pinned: false
|
| 10 |
license: mit
|
| 11 |
---
|
| 12 |
|
| 13 |

|
| 14 |
|
| 15 |
-
**One for All** distills
|
| 16 |
|
| 17 |
### Tabs
|
| 18 |
|
| 19 |
-
- **∀
|
| 20 |
-
- **
|
| 21 |
-
- **
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
colorFrom: indigo
|
| 5 |
colorTo: purple
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: "6.18.0"
|
| 8 |
+
app_file: server_app.py
|
| 9 |
pinned: false
|
| 10 |
license: mit
|
| 11 |
---
|
| 12 |
|
| 13 |

|
| 14 |
|
| 15 |
+
**One for All** distills 6 heterogeneous teacher LLMs (Qwen2.5-1.5B, SmolLM2-1.7B, Phi-3.5-mini, gemma-2-2b-it, MiniCPM-2B, Nemotron-Mini-4B) into a single Qwen2.5-0.5B student via gated CKA geometry distillation (Path B — geometry-only, tokenizer-agnostic).
|
| 16 |
|
| 17 |
### Tabs
|
| 18 |
|
| 19 |
+
- **∀ Souls** — 3D UMAP soul space of all models over 24 probe texts. Type a prompt and watch the student answer **token by token** while the gate bars show, live, which teacher's geometry is dominant. The pooled prompt lands as a new point in soul space.
|
| 20 |
+
- **⚔ Arena** — one prompt, two models: the untouched Qwen2.5-0.5B base races the distilled student side by side (same weights, LoRA off vs on).
|
| 21 |
+
- **⬡ Geometry** — CKA alignment heatmap across all model pairs.
|
| 22 |
+
- **↗ Training** — Loss curves and gate evolution over training steps.
|
| 23 |
+
|
| 24 |
+
### UI
|
| 25 |
+
|
| 26 |
+
`server_app.py` is a **custom frontend on `gr.Server`** 🎨 — our own HTML/JS/Three.js
|
| 27 |
+
(`frontend/`) talking to Gradio API endpoints via `@gradio/client`. The legacy
|
| 28 |
+
Blocks UI still works: set `app_file: app.py` (it also runs on sdk 6.18).
|
| 29 |
+
|
| 30 |
+
### Backends
|
| 31 |
+
|
| 32 |
+
- default — torch + ZeroGPU (per-token gates).
|
| 33 |
+
- `OFA_BACKEND=llamacpp` — the student runs as a **GGUF through llama.cpp** 🦙 (CPU-friendly; gates computed on the prompt embedding via numpy, no torch in the hot path).
|
_boot.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
space/_boot.py — shared startup + backend dispatch for both Space entrypoints.
|
| 3 |
+
|
| 4 |
+
app.py (Gradio Blocks, legacy) and server_app.py (gr.Server + custom frontend)
|
| 5 |
+
load the exact same runtime: viz data, UMAP reducer, and the student in either
|
| 6 |
+
backend (torch / llamacpp). Keeping it here means the two UIs can't drift.
|
| 7 |
+
"""
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
from dataclasses import dataclass, field
|
| 12 |
+
from typing import Any
|
| 13 |
+
|
| 14 |
+
import _data
|
| 15 |
+
import _probe
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@dataclass
|
| 19 |
+
class Runtime:
|
| 20 |
+
hf_token: str | None = None
|
| 21 |
+
backend: str = "torch"
|
| 22 |
+
viz: dict = field(default_factory=dict)
|
| 23 |
+
reducer: Any = None
|
| 24 |
+
coords3d: Any = None
|
| 25 |
+
tok: Any = None
|
| 26 |
+
student: Any = None
|
| 27 |
+
gating: Any = None
|
| 28 |
+
lcs: Any = None
|
| 29 |
+
model_ready: bool = False
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def load_runtime() -> Runtime:
|
| 33 |
+
rt = Runtime(hf_token=os.environ.get("HF_TOKEN"), backend=_probe.BACKEND)
|
| 34 |
+
|
| 35 |
+
local_viz = os.environ.get("VIZ_DATA_PATH")
|
| 36 |
+
try:
|
| 37 |
+
if local_viz:
|
| 38 |
+
rt.viz = _data.load_from_path(local_viz)
|
| 39 |
+
print(f"[ofa-space] loaded viz from {local_viz}")
|
| 40 |
+
else:
|
| 41 |
+
rt.viz = _data.load_and_parse(rt.hf_token)
|
| 42 |
+
except Exception as e:
|
| 43 |
+
print(f"[ofa-space] viz_data.json not available ({e}), using empty state")
|
| 44 |
+
rt.viz = _data.make_empty_viz()
|
| 45 |
+
|
| 46 |
+
try:
|
| 47 |
+
if rt.viz["stacked"].shape[0] > 3:
|
| 48 |
+
rt.reducer = _data.fit_umap3d(rt.viz["stacked"])
|
| 49 |
+
rt.coords3d = rt.reducer.embedding_
|
| 50 |
+
print(f"[ofa-space] UMAP done: {rt.coords3d.shape}")
|
| 51 |
+
else:
|
| 52 |
+
print(f"[ofa-space] not enough points for UMAP: {rt.viz['stacked'].shape[0]}")
|
| 53 |
+
except Exception as e:
|
| 54 |
+
print(f"[ofa-space] UMAP failed ({e}), 3D disabled")
|
| 55 |
+
rt.reducer = rt.coords3d = None
|
| 56 |
+
|
| 57 |
+
try:
|
| 58 |
+
if rt.backend == "llamacpp":
|
| 59 |
+
rt.lcs = _probe.load_student_llamacpp(rt.hf_token)
|
| 60 |
+
print(f"[ofa-space] llama.cpp backend ready ({_probe.GGUF_FILE})")
|
| 61 |
+
else:
|
| 62 |
+
rt.tok, rt.student, rt.gating = _probe.load_student(rt.hf_token)
|
| 63 |
+
rt.model_ready = True
|
| 64 |
+
except Exception as e:
|
| 65 |
+
print(f"[ofa-space] Student not available ({e}). Probe disabled.")
|
| 66 |
+
rt.model_ready = False
|
| 67 |
+
|
| 68 |
+
return rt
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
# ── Backend dispatch: same call sites drive torch and llama.cpp ────────────
|
| 72 |
+
|
| 73 |
+
def to_device(rt: Runtime) -> None:
|
| 74 |
+
if rt.backend != "llamacpp" and rt.student is not None:
|
| 75 |
+
device = "cuda" if __import__("torch").cuda.is_available() else "cpu"
|
| 76 |
+
rt.student.to(device)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def stream(rt: Runtime, text: str):
|
| 80 |
+
if rt.backend == "llamacpp":
|
| 81 |
+
return _probe.stream_generate_llamacpp(text, rt.lcs)
|
| 82 |
+
return _probe.stream_generate(text, rt.student, rt.tok, rt.gating)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def stream_pair(rt: Runtime, text: str):
|
| 86 |
+
if rt.backend == "llamacpp":
|
| 87 |
+
return _probe.stream_pair_llamacpp(text, rt.lcs)
|
| 88 |
+
return _probe.stream_pair(text, rt.student, rt.tok, rt.gating)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def final_probe(rt: Runtime, text: str):
|
| 92 |
+
if rt.backend == "llamacpp":
|
| 93 |
+
return _probe.run_probe_llamacpp(text, rt.lcs, rt.reducer)
|
| 94 |
+
return _probe.run_probe(text, rt.student, rt.tok, rt.gating, rt.reducer)
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
# ── Payload for the custom frontend (server_app.py) ────────────────────────
|
| 98 |
+
|
| 99 |
+
# Frontend palette — student first, then teachers (qwen, smollm, phi, gemma,
|
| 100 |
+
# minicpm, nemotron). No purple by design; the legacy Blocks UI keeps its own
|
| 101 |
+
# palette in _fig.py.
|
| 102 |
+
MODEL_COLORS = ["#e6edf3", "#38bdf8", "#f59e0b", "#f43f5e", "#2dd4bf", "#f472b6", "#76b900"]
|
| 103 |
+
|
| 104 |
+
# Canonical 6-teacher lineup. viz_data.json exported from an older 5-teacher
|
| 105 |
+
# run is a prefix of this — pad it so the UI already shows nemotron (its meter
|
| 106 |
+
# stays at 0 until a 6-gate checkpoint is published).
|
| 107 |
+
CANONICAL_TEACHERS = ["qwen", "smollm", "phi", "gemma", "minicpm", "nemotron"]
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def viz_payload(viz: dict, coords3d, backend: str = "torch",
|
| 111 |
+
model_ready: bool = False) -> dict:
|
| 112 |
+
"""Everything the JS frontend needs in one JSON-safe dict."""
|
| 113 |
+
models = []
|
| 114 |
+
if coords3d is not None and viz.get("labels"):
|
| 115 |
+
labels = viz["labels"]
|
| 116 |
+
for i, name in enumerate(viz.get("model_names", [])):
|
| 117 |
+
pts = [list(map(float, coords3d[j]))
|
| 118 |
+
for j, lab in enumerate(labels) if lab == name]
|
| 119 |
+
models.append({
|
| 120 |
+
"name": name,
|
| 121 |
+
"color": MODEL_COLORS[i % len(MODEL_COLORS)],
|
| 122 |
+
"points": pts,
|
| 123 |
+
})
|
| 124 |
+
teachers = viz.get("teacher_names", [])
|
| 125 |
+
if teachers == CANONICAL_TEACHERS[:len(teachers)]:
|
| 126 |
+
teachers = CANONICAL_TEACHERS
|
| 127 |
+
return {
|
| 128 |
+
"models": models,
|
| 129 |
+
"teachers": teachers,
|
| 130 |
+
"teacher_colors": [MODEL_COLORS[1 + i % (len(MODEL_COLORS) - 1)]
|
| 131 |
+
for i in range(len(teachers))],
|
| 132 |
+
"cka": viz.get("cka", {}),
|
| 133 |
+
"curves": viz.get("curves", {}),
|
| 134 |
+
"backend": backend,
|
| 135 |
+
"model_ready": model_ready,
|
| 136 |
+
}
|
_fig.py
CHANGED
|
@@ -11,7 +11,7 @@ DARK = dict(
|
|
| 11 |
GRID_COLOR = "#21262d"
|
| 12 |
|
| 13 |
# index 0 = student, 1-5 = teachers
|
| 14 |
-
MODEL_COLORS = ["#e6edf3", "#7c3aed", "#06b6d4", "#f59e0b", "#34d399", "#f472b6"]
|
| 15 |
|
| 16 |
|
| 17 |
def build_base_traces(viz: dict, coords3d: np.ndarray) -> list:
|
|
|
|
| 11 |
GRID_COLOR = "#21262d"
|
| 12 |
|
| 13 |
# index 0 = student, 1-5 = teachers
|
| 14 |
+
MODEL_COLORS = ["#e6edf3", "#7c3aed", "#06b6d4", "#f59e0b", "#34d399", "#f472b6", "#76b900"]
|
| 15 |
|
| 16 |
|
| 17 |
def build_base_traces(viz: dict, coords3d: np.ndarray) -> list:
|
_glb.py
CHANGED
|
@@ -30,6 +30,7 @@ _COLORS_RGB: list[tuple[int, int, int]] = [
|
|
| 30 |
(245, 158, 11), # teacher2 — #f59e0b
|
| 31 |
( 52, 211, 153), # teacher3 — #34d399
|
| 32 |
(244, 114, 182), # teacher4 — #f472b6
|
|
|
|
| 33 |
]
|
| 34 |
_PROBE_COLOR = (255, 255, 255)
|
| 35 |
|
|
|
|
| 30 |
(245, 158, 11), # teacher2 — #f59e0b
|
| 31 |
( 52, 211, 153), # teacher3 — #34d399
|
| 32 |
(244, 114, 182), # teacher4 — #f472b6
|
| 33 |
+
(118, 185, 0), # teacher5 — #76b900 (nemotron / NVIDIA green)
|
| 34 |
]
|
| 35 |
_PROBE_COLOR = (255, 255, 255)
|
| 36 |
|
_html.py
CHANGED
|
@@ -1,18 +1,18 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
-
# index 0-
|
| 4 |
-
_TEACHER_COLORS = ["#7c3aed", "#06b6d4", "#f59e0b", "#34d399", "#f472b6"]
|
| 5 |
|
| 6 |
|
| 7 |
-
def gate_html(gate_weights: list[float], teacher_names: list[str]
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
rows = []
|
| 15 |
-
for orig_idx, (w, name) in
|
| 16 |
color = _TEACHER_COLORS[orig_idx % len(_TEACHER_COLORS)]
|
| 17 |
bar_pct = int(w * 100)
|
| 18 |
rows.append(
|
|
@@ -20,7 +20,8 @@ def gate_html(gate_weights: list[float], teacher_names: list[str]) -> str:
|
|
| 20 |
f'<span style="width:80px;font-family:monospace;font-size:11px;color:{color};'
|
| 21 |
f'white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{name}</span>'
|
| 22 |
f'<div style="flex:1;height:5px;background:#21262d;border-radius:3px;overflow:hidden;">'
|
| 23 |
-
f'<div style="width:{bar_pct}%;height:100%;background:{color};border-radius:3px;
|
|
|
|
| 24 |
f'</div>'
|
| 25 |
f'<span style="width:36px;font-family:monospace;font-size:11px;'
|
| 26 |
f'color:#8b949e;text-align:right;">{w:.2f}</span>'
|
|
@@ -63,7 +64,7 @@ def task_html(gate_weights: list[float], teacher_names: list[str]) -> str:
|
|
| 63 |
)
|
| 64 |
|
| 65 |
|
| 66 |
-
def header_html() -> str:
|
| 67 |
"""App header with ∀ logo, wordmark, and status pills."""
|
| 68 |
def pill(color: str, dot_color: str, text: str, pulse: bool = False) -> str:
|
| 69 |
dot_class = ' class="live-dot"' if pulse else ''
|
|
@@ -105,8 +106,9 @@ def header_html() -> str:
|
|
| 105 |
# Right: pills
|
| 106 |
'<div style="display:flex;gap:8px;flex-wrap:wrap;">'
|
| 107 |
+ pill("#06b6d4", "#06b6d4", "STUDENT · Qwen2.5-0.5B", pulse=True)
|
| 108 |
-
+ pill("#7c3aed", "#7c3aed", "
|
| 109 |
+ pill("#f59e0b", "#f59e0b", "PATH B · GEOMETRY")
|
|
|
|
| 110 |
+ '</div></div>'
|
| 111 |
|
| 112 |
# Divider with glow
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
+
# index 0-5 = teachers (aligns with MODEL_COLORS[1:] in _fig.py)
|
| 4 |
+
_TEACHER_COLORS = ["#7c3aed", "#06b6d4", "#f59e0b", "#34d399", "#f472b6", "#76b900"]
|
| 5 |
|
| 6 |
|
| 7 |
+
def gate_html(gate_weights: list[float], teacher_names: list[str],
|
| 8 |
+
ranked: bool = True) -> str:
|
| 9 |
+
"""Horizontal bar chart. ranked=False keeps teacher order fixed — used
|
| 10 |
+
while streaming so bars animate in place instead of swapping rows."""
|
| 11 |
+
pairs = list(enumerate(zip(gate_weights, teacher_names)))
|
| 12 |
+
if ranked:
|
| 13 |
+
pairs.sort(key=lambda x: x[1][0], reverse=True)
|
| 14 |
rows = []
|
| 15 |
+
for orig_idx, (w, name) in pairs:
|
| 16 |
color = _TEACHER_COLORS[orig_idx % len(_TEACHER_COLORS)]
|
| 17 |
bar_pct = int(w * 100)
|
| 18 |
rows.append(
|
|
|
|
| 20 |
f'<span style="width:80px;font-family:monospace;font-size:11px;color:{color};'
|
| 21 |
f'white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{name}</span>'
|
| 22 |
f'<div style="flex:1;height:5px;background:#21262d;border-radius:3px;overflow:hidden;">'
|
| 23 |
+
f'<div style="width:{bar_pct}%;height:100%;background:{color};border-radius:3px;'
|
| 24 |
+
f'transition:width 0.18s ease;"></div>'
|
| 25 |
f'</div>'
|
| 26 |
f'<span style="width:36px;font-family:monospace;font-size:11px;'
|
| 27 |
f'color:#8b949e;text-align:right;">{w:.2f}</span>'
|
|
|
|
| 64 |
)
|
| 65 |
|
| 66 |
|
| 67 |
+
def header_html(n_teachers: int = 6, backend: str = "torch") -> str:
|
| 68 |
"""App header with ∀ logo, wordmark, and status pills."""
|
| 69 |
def pill(color: str, dot_color: str, text: str, pulse: bool = False) -> str:
|
| 70 |
dot_class = ' class="live-dot"' if pulse else ''
|
|
|
|
| 106 |
# Right: pills
|
| 107 |
'<div style="display:flex;gap:8px;flex-wrap:wrap;">'
|
| 108 |
+ pill("#06b6d4", "#06b6d4", "STUDENT · Qwen2.5-0.5B", pulse=True)
|
| 109 |
+
+ pill("#7c3aed", "#7c3aed", f"{n_teachers} TEACHERS")
|
| 110 |
+ pill("#f59e0b", "#f59e0b", "PATH B · GEOMETRY")
|
| 111 |
+
+ (pill("#76b900", "#76b900", "🦙 llama.cpp · GGUF") if backend == "llamacpp" else "")
|
| 112 |
+ '</div></div>'
|
| 113 |
|
| 114 |
# Divider with glow
|
_probe.py
CHANGED
|
@@ -8,7 +8,13 @@ import numpy as np
|
|
| 8 |
STUDENT_BASE = "Qwen/Qwen2.5-0.5B-Instruct"
|
| 9 |
ADAPTER_REPO = "build-small-hackathon/deku"
|
| 10 |
STUDENT_HIDDEN_DIM = 896 # Qwen2.5-0.5B hidden size
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
|
| 14 |
class GatingNetwork(nn.Module):
|
|
@@ -55,13 +61,32 @@ def load_student(hf_token: str | None = None):
|
|
| 55 |
repo_type="model",
|
| 56 |
token=token,
|
| 57 |
)
|
| 58 |
-
gating =
|
| 59 |
-
gating.load_state_dict(torch.load(gating_path, map_location="cpu"))
|
| 60 |
-
gating.eval()
|
| 61 |
|
| 62 |
return tok, student, gating
|
| 63 |
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
def generate_response(
|
| 66 |
text: str,
|
| 67 |
student: nn.Module,
|
|
@@ -70,15 +95,7 @@ def generate_response(
|
|
| 70 |
) -> str:
|
| 71 |
"""Run student generation and return decoded answer text."""
|
| 72 |
device = next(student.parameters()).device
|
| 73 |
-
|
| 74 |
-
prompt = tok.apply_chat_template(
|
| 75 |
-
[{"role": "user", "content": text}],
|
| 76 |
-
tokenize=False,
|
| 77 |
-
add_generation_prompt=True,
|
| 78 |
-
)
|
| 79 |
-
except Exception:
|
| 80 |
-
prompt = text
|
| 81 |
-
enc = tok(prompt, return_tensors="pt").to(device)
|
| 82 |
with torch.no_grad():
|
| 83 |
out = student.generate(
|
| 84 |
input_ids=enc["input_ids"],
|
|
@@ -91,6 +108,121 @@ def generate_response(
|
|
| 91 |
return tok.decode(new_tokens, skip_special_tokens=True)
|
| 92 |
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
def run_probe(
|
| 95 |
text: str,
|
| 96 |
student: nn.Module,
|
|
@@ -121,3 +253,111 @@ def run_probe(
|
|
| 121 |
"z": float(coords3d[0, 2]),
|
| 122 |
"label": "probe",
|
| 123 |
}, gate_weights
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
STUDENT_BASE = "Qwen/Qwen2.5-0.5B-Instruct"
|
| 9 |
ADAPTER_REPO = "build-small-hackathon/deku"
|
| 10 |
STUDENT_HIDDEN_DIM = 896 # Qwen2.5-0.5B hidden size
|
| 11 |
+
|
| 12 |
+
# Backend: "torch" (default) or "llamacpp" (GGUF via llama-cpp-python, CPU-friendly).
|
| 13 |
+
BACKEND = os.environ.get("OFA_BACKEND", "torch").lower()
|
| 14 |
+
GGUF_REPO = os.environ.get("OFA_GGUF_REPO", "build-small-hackathon/deku-gguf")
|
| 15 |
+
GGUF_FILE = os.environ.get("OFA_GGUF_FILE", "deku-q8_0.gguf")
|
| 16 |
+
BASE_GGUF_REPO = os.environ.get("OFA_BASE_GGUF_REPO", "Qwen/Qwen2.5-0.5B-Instruct-GGUF")
|
| 17 |
+
BASE_GGUF_FILE = os.environ.get("OFA_BASE_GGUF_FILE", "qwen2.5-0.5b-instruct-q8_0.gguf")
|
| 18 |
|
| 19 |
|
| 20 |
class GatingNetwork(nn.Module):
|
|
|
|
| 61 |
repo_type="model",
|
| 62 |
token=token,
|
| 63 |
)
|
| 64 |
+
gating = _gating_from_state(torch.load(gating_path, map_location="cpu"))
|
|
|
|
|
|
|
| 65 |
|
| 66 |
return tok, student, gating
|
| 67 |
|
| 68 |
|
| 69 |
+
def _gating_from_state(state: dict) -> GatingNetwork:
|
| 70 |
+
"""Build the gate from a checkpoint, inferring n_teachers from the weight
|
| 71 |
+
shape — keeps the Space compatible with 5- and 6-teacher runs."""
|
| 72 |
+
n_teachers, hidden = state["fc.weight"].shape
|
| 73 |
+
gating = GatingNetwork(hidden, n_teachers)
|
| 74 |
+
gating.load_state_dict(state)
|
| 75 |
+
gating.eval()
|
| 76 |
+
return gating
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _chat_prompt(text: str, tok) -> str:
|
| 80 |
+
try:
|
| 81 |
+
return tok.apply_chat_template(
|
| 82 |
+
[{"role": "user", "content": text}],
|
| 83 |
+
tokenize=False,
|
| 84 |
+
add_generation_prompt=True,
|
| 85 |
+
)
|
| 86 |
+
except Exception:
|
| 87 |
+
return text
|
| 88 |
+
|
| 89 |
+
|
| 90 |
def generate_response(
|
| 91 |
text: str,
|
| 92 |
student: nn.Module,
|
|
|
|
| 95 |
) -> str:
|
| 96 |
"""Run student generation and return decoded answer text."""
|
| 97 |
device = next(student.parameters()).device
|
| 98 |
+
enc = tok(_chat_prompt(text, tok), return_tensors="pt").to(device)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
with torch.no_grad():
|
| 100 |
out = student.generate(
|
| 101 |
input_ids=enc["input_ids"],
|
|
|
|
| 108 |
return tok.decode(new_tokens, skip_special_tokens=True)
|
| 109 |
|
| 110 |
|
| 111 |
+
def _decode_step(student: nn.Module, ids: torch.Tensor, past):
|
| 112 |
+
"""One greedy decode step. Returns (next_id, new_past, last_hidden)."""
|
| 113 |
+
with torch.no_grad():
|
| 114 |
+
out = student(
|
| 115 |
+
input_ids=ids,
|
| 116 |
+
past_key_values=past,
|
| 117 |
+
use_cache=True,
|
| 118 |
+
output_hidden_states=True,
|
| 119 |
+
)
|
| 120 |
+
next_id = out.logits[:, -1, :].argmax(dim=-1, keepdim=True)
|
| 121 |
+
h_last = out.hidden_states[-1][:, -1, :].float()
|
| 122 |
+
return next_id, out.past_key_values, h_last
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def _adapter_off(student: nn.Module):
|
| 126 |
+
"""disable_adapter() ctx on PeftModel; no-op for plain models (tests)."""
|
| 127 |
+
disable = getattr(student, "disable_adapter", None)
|
| 128 |
+
if disable is not None:
|
| 129 |
+
return disable()
|
| 130 |
+
import contextlib
|
| 131 |
+
return contextlib.nullcontext()
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def stream_generate(
|
| 135 |
+
text: str,
|
| 136 |
+
student: nn.Module,
|
| 137 |
+
tok,
|
| 138 |
+
gating: GatingNetwork,
|
| 139 |
+
max_new_tokens: int = 200,
|
| 140 |
+
ema: float = 0.3,
|
| 141 |
+
):
|
| 142 |
+
"""Greedy decode loop that exposes the gate at every step.
|
| 143 |
+
|
| 144 |
+
Yields (partial_text, gate_weights) per generated token. Gate weights are
|
| 145 |
+
EMA-smoothed so the live bars move instead of flickering. model.generate()
|
| 146 |
+
can't do this — it doesn't surface hidden states mid-stream.
|
| 147 |
+
"""
|
| 148 |
+
device = next(student.parameters()).device
|
| 149 |
+
enc = tok(_chat_prompt(text, tok), return_tensors="pt")
|
| 150 |
+
ids = enc["input_ids"].to(device)
|
| 151 |
+
past = None
|
| 152 |
+
gate_smooth: torch.Tensor | None = None
|
| 153 |
+
out_ids: list[int] = []
|
| 154 |
+
for _ in range(max_new_tokens):
|
| 155 |
+
next_id, past, h_last = _decode_step(student, ids, past)
|
| 156 |
+
with torch.no_grad():
|
| 157 |
+
g = gating(h_last).squeeze(0)
|
| 158 |
+
gate_smooth = g if gate_smooth is None else ema * g + (1 - ema) * gate_smooth
|
| 159 |
+
if next_id.item() == tok.eos_token_id:
|
| 160 |
+
break
|
| 161 |
+
out_ids.append(next_id.item())
|
| 162 |
+
ids = next_id
|
| 163 |
+
yield (
|
| 164 |
+
tok.decode(out_ids, skip_special_tokens=True),
|
| 165 |
+
gate_smooth.tolist(),
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def stream_pair(
|
| 170 |
+
text: str,
|
| 171 |
+
student: nn.Module,
|
| 172 |
+
tok,
|
| 173 |
+
gating: GatingNetwork,
|
| 174 |
+
max_new_tokens: int = 160,
|
| 175 |
+
ema: float = 0.3,
|
| 176 |
+
):
|
| 177 |
+
"""Race base (LoRA off) vs deku (LoRA on) — one token each per round.
|
| 178 |
+
|
| 179 |
+
Same PeftModel serves both sides via disable_adapter(), so no extra
|
| 180 |
+
memory. Yields (base_text, deku_text, gate_weights); gates come from the
|
| 181 |
+
deku side only.
|
| 182 |
+
"""
|
| 183 |
+
device = next(student.parameters()).device
|
| 184 |
+
enc = tok(_chat_prompt(text, tok), return_tensors="pt")
|
| 185 |
+
prompt_ids = enc["input_ids"].to(device)
|
| 186 |
+
|
| 187 |
+
sides = {
|
| 188 |
+
"base": {"ids": prompt_ids, "past": None, "out": [], "done": False},
|
| 189 |
+
"deku": {"ids": prompt_ids, "past": None, "out": [], "done": False},
|
| 190 |
+
}
|
| 191 |
+
gate_smooth: torch.Tensor | None = None
|
| 192 |
+
|
| 193 |
+
for _ in range(max_new_tokens):
|
| 194 |
+
if not sides["base"]["done"]:
|
| 195 |
+
s = sides["base"]
|
| 196 |
+
with _adapter_off(student):
|
| 197 |
+
next_id, s["past"], _ = _decode_step(student, s["ids"], s["past"])
|
| 198 |
+
if next_id.item() == tok.eos_token_id:
|
| 199 |
+
s["done"] = True
|
| 200 |
+
else:
|
| 201 |
+
s["out"].append(next_id.item())
|
| 202 |
+
s["ids"] = next_id
|
| 203 |
+
|
| 204 |
+
if not sides["deku"]["done"]:
|
| 205 |
+
s = sides["deku"]
|
| 206 |
+
next_id, s["past"], h_last = _decode_step(student, s["ids"], s["past"])
|
| 207 |
+
with torch.no_grad():
|
| 208 |
+
g = gating(h_last).squeeze(0)
|
| 209 |
+
gate_smooth = g if gate_smooth is None else ema * g + (1 - ema) * gate_smooth
|
| 210 |
+
if next_id.item() == tok.eos_token_id:
|
| 211 |
+
s["done"] = True
|
| 212 |
+
else:
|
| 213 |
+
s["out"].append(next_id.item())
|
| 214 |
+
s["ids"] = next_id
|
| 215 |
+
|
| 216 |
+
if sides["base"]["done"] and sides["deku"]["done"]:
|
| 217 |
+
break
|
| 218 |
+
yield (
|
| 219 |
+
tok.decode(sides["base"]["out"], skip_special_tokens=True),
|
| 220 |
+
tok.decode(sides["deku"]["out"], skip_special_tokens=True),
|
| 221 |
+
gate_smooth.tolist() if gate_smooth is not None
|
| 222 |
+
else [1.0 / gating.fc.out_features] * gating.fc.out_features,
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
|
| 226 |
def run_probe(
|
| 227 |
text: str,
|
| 228 |
student: nn.Module,
|
|
|
|
| 253 |
"z": float(coords3d[0, 2]),
|
| 254 |
"label": "probe",
|
| 255 |
}, gate_weights
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
# ── llama.cpp backend (🦙 badge — GGUF student, runs on CPU, no torch path) ──
|
| 259 |
+
|
| 260 |
+
def _np_softmax(x: np.ndarray) -> np.ndarray:
|
| 261 |
+
e = np.exp(x - x.max())
|
| 262 |
+
return e / e.sum()
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
def gate_from_embedding(emb: np.ndarray, weight: np.ndarray, bias: np.ndarray) -> list[float]:
|
| 266 |
+
"""Gate in pure numpy: softmax(W @ e + b). Mirrors GatingNetwork.forward."""
|
| 267 |
+
return _np_softmax(weight @ emb + bias).tolist()
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
class LlamaCppStudent:
|
| 271 |
+
"""deku as GGUF: one llama.cpp instance for generation, one in embedding
|
| 272 |
+
mode (mean pooling) for the gate / soul-space probe, numpy gate weights.
|
| 273 |
+
The base model (Arena) is downloaded lazily on first use."""
|
| 274 |
+
|
| 275 |
+
def __init__(self, gen, emb, gate_w: np.ndarray, gate_b: np.ndarray, token=None):
|
| 276 |
+
self.gen = gen
|
| 277 |
+
self.emb = emb
|
| 278 |
+
self.gate_w = gate_w
|
| 279 |
+
self.gate_b = gate_b
|
| 280 |
+
self.base = None
|
| 281 |
+
self._token = token
|
| 282 |
+
|
| 283 |
+
@property
|
| 284 |
+
def n_teachers(self) -> int:
|
| 285 |
+
return self.gate_w.shape[0]
|
| 286 |
+
|
| 287 |
+
def embed(self, text: str) -> np.ndarray:
|
| 288 |
+
return np.asarray(self.emb.embed(text, normalize=False), dtype=np.float32)
|
| 289 |
+
|
| 290 |
+
def prompt_gates(self, text: str) -> list[float]:
|
| 291 |
+
return gate_from_embedding(self.embed(text), self.gate_w, self.gate_b)
|
| 292 |
+
|
| 293 |
+
def ensure_base(self) -> None:
|
| 294 |
+
if self.base is None:
|
| 295 |
+
from llama_cpp import Llama
|
| 296 |
+
from huggingface_hub import hf_hub_download
|
| 297 |
+
path = hf_hub_download(BASE_GGUF_REPO, BASE_GGUF_FILE, token=self._token)
|
| 298 |
+
self.base = Llama(model_path=path, n_ctx=2048, verbose=False)
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
def load_student_llamacpp(hf_token: str | None = None) -> LlamaCppStudent:
|
| 302 |
+
from llama_cpp import Llama
|
| 303 |
+
from huggingface_hub import hf_hub_download
|
| 304 |
+
|
| 305 |
+
token = hf_token or os.environ.get("HF_TOKEN")
|
| 306 |
+
gguf = hf_hub_download(GGUF_REPO, GGUF_FILE, token=token)
|
| 307 |
+
npz = np.load(hf_hub_download(GGUF_REPO, "gating.npz", token=token))
|
| 308 |
+
gen = Llama(model_path=gguf, n_ctx=2048, verbose=False)
|
| 309 |
+
emb = Llama(model_path=gguf, n_ctx=2048, embedding=True, verbose=False)
|
| 310 |
+
return LlamaCppStudent(gen, emb,
|
| 311 |
+
npz["weight"].astype(np.float32),
|
| 312 |
+
npz["bias"].astype(np.float32), token=token)
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
def _chat_stream(llm, text: str, max_tokens: int):
|
| 316 |
+
"""Yield accumulated text from a llama.cpp chat-completion stream."""
|
| 317 |
+
pieces: list[str] = []
|
| 318 |
+
for chunk in llm.create_chat_completion(
|
| 319 |
+
messages=[{"role": "user", "content": text}],
|
| 320 |
+
max_tokens=max_tokens, temperature=0.0, stream=True,
|
| 321 |
+
):
|
| 322 |
+
delta = chunk["choices"][0]["delta"].get("content")
|
| 323 |
+
if delta:
|
| 324 |
+
pieces.append(delta)
|
| 325 |
+
yield "".join(pieces)
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
def stream_generate_llamacpp(text: str, lcs: LlamaCppStudent, max_tokens: int = 200):
|
| 329 |
+
"""Yields (partial_text, gate_weights). llama.cpp doesn't expose hidden
|
| 330 |
+
states mid-generation, so gates are computed once on the prompt embedding
|
| 331 |
+
(vs per-token in the torch path) — documented drift, same gate network."""
|
| 332 |
+
gates = lcs.prompt_gates(text)
|
| 333 |
+
for partial in _chat_stream(lcs.gen, text, max_tokens):
|
| 334 |
+
yield partial, gates
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
def stream_pair_llamacpp(text: str, lcs: LlamaCppStudent, max_tokens: int = 160):
|
| 338 |
+
"""Arena on llama.cpp: deku GGUF vs official base GGUF, interleaved.
|
| 339 |
+
Yields (base_text, deku_text, gate_weights)."""
|
| 340 |
+
import itertools
|
| 341 |
+
lcs.ensure_base()
|
| 342 |
+
gates = lcs.prompt_gates(text)
|
| 343 |
+
base_txt, deku_txt = "", ""
|
| 344 |
+
for b, d in itertools.zip_longest(
|
| 345 |
+
_chat_stream(lcs.base, text, max_tokens),
|
| 346 |
+
_chat_stream(lcs.gen, text, max_tokens),
|
| 347 |
+
):
|
| 348 |
+
base_txt = b if b is not None else base_txt
|
| 349 |
+
deku_txt = d if d is not None else deku_txt
|
| 350 |
+
yield base_txt, deku_txt, gates
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
def run_probe_llamacpp(text: str, lcs: LlamaCppStudent, reducer) -> tuple[dict, list[float]]:
|
| 354 |
+
"""llama.cpp analogue of run_probe: mean-pooled embedding → gate + UMAP point."""
|
| 355 |
+
pooled = lcs.embed(text)
|
| 356 |
+
gates = gate_from_embedding(pooled, lcs.gate_w, lcs.gate_b)
|
| 357 |
+
coords3d = reducer.transform(pooled[None, :])
|
| 358 |
+
return {
|
| 359 |
+
"x": float(coords3d[0, 0]),
|
| 360 |
+
"y": float(coords3d[0, 1]),
|
| 361 |
+
"z": float(coords3d[0, 2]),
|
| 362 |
+
"label": "probe",
|
| 363 |
+
}, gates
|
_three.py
CHANGED
|
@@ -13,7 +13,7 @@ import html as _html
|
|
| 13 |
import json
|
| 14 |
import numpy as np
|
| 15 |
|
| 16 |
-
MODEL_COLORS = ["#e6edf3", "#7c3aed", "#06b6d4", "#f59e0b", "#34d399", "#f472b6"]
|
| 17 |
|
| 18 |
_EMPTY = """
|
| 19 |
<div style="width:100%;height:520px;background:#0d1117;border-radius:8px;
|
|
|
|
| 13 |
import json
|
| 14 |
import numpy as np
|
| 15 |
|
| 16 |
+
MODEL_COLORS = ["#e6edf3", "#7c3aed", "#06b6d4", "#f59e0b", "#34d399", "#f472b6", "#76b900"]
|
| 17 |
|
| 18 |
_EMPTY = """
|
| 19 |
<div style="width:100%;height:520px;background:#0d1117;border-radius:8px;
|
app.py
CHANGED
|
@@ -10,47 +10,20 @@ import html as _html_stdlib
|
|
| 10 |
import gradio as gr
|
| 11 |
import spaces
|
| 12 |
|
| 13 |
-
import _data
|
| 14 |
import _fig
|
| 15 |
import _glb
|
| 16 |
import _html
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
VIZ = _data.load_and_parse(HF_TOKEN)
|
| 29 |
-
except Exception as e:
|
| 30 |
-
print(f"[ofa-space] viz_data.json not available ({e}), using empty state")
|
| 31 |
-
VIZ = _data.make_empty_viz()
|
| 32 |
-
|
| 33 |
-
try:
|
| 34 |
-
if VIZ["stacked"].shape[0] > 3:
|
| 35 |
-
REDUCER = _data.fit_umap3d(VIZ["stacked"])
|
| 36 |
-
COORDS3D = REDUCER.embedding_
|
| 37 |
-
print(f"[ofa-space] UMAP done: {COORDS3D.shape}")
|
| 38 |
-
else:
|
| 39 |
-
print(f"[ofa-space] not enough points for UMAP: {VIZ['stacked'].shape[0]}")
|
| 40 |
-
REDUCER = None
|
| 41 |
-
COORDS3D = None
|
| 42 |
-
except Exception as e:
|
| 43 |
-
print(f"[ofa-space] UMAP failed ({e}), 3D disabled")
|
| 44 |
-
REDUCER = None
|
| 45 |
-
COORDS3D = None
|
| 46 |
-
|
| 47 |
-
try:
|
| 48 |
-
TOK, STUDENT, GATING = _probe.load_student(HF_TOKEN)
|
| 49 |
-
_MODEL_READY = True
|
| 50 |
-
except Exception as e:
|
| 51 |
-
print(f"[ofa-space] Student not available ({e}). Probe disabled.")
|
| 52 |
-
TOK = STUDENT = GATING = None
|
| 53 |
-
_MODEL_READY = False
|
| 54 |
|
| 55 |
_INIT_GLB = _glb.build_glb(VIZ, COORDS3D, [])
|
| 56 |
print(f"[ofa-space] GLB path: {_INIT_GLB}")
|
|
@@ -66,37 +39,86 @@ else:
|
|
| 66 |
_CAM = (45, 30, 10)
|
| 67 |
|
| 68 |
|
| 69 |
-
def _response_html(text: str) -> str:
|
| 70 |
safe = _html_stdlib.escape(text).replace("\n", "<br>")
|
| 71 |
return (
|
| 72 |
'<div style="background:#0d1117;border:1px solid #30363d;border-radius:6px;'
|
| 73 |
'padding:14px;margin-top:8px;">'
|
| 74 |
-
'<div style="font-size:10px;color:
|
| 75 |
-
'margin-bottom:8px;letter-spacing:0.04em;">
|
| 76 |
f'<div style="font-size:13px;color:#e6edf3;line-height:1.65;">{safe}</div>'
|
| 77 |
"</div>"
|
| 78 |
)
|
| 79 |
|
| 80 |
|
| 81 |
-
# ──
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
@spaces.GPU
|
| 83 |
-
def probe_fn(text: str, probe_points: list)
|
| 84 |
-
no_change = _glb.build_glb(VIZ, COORDS3D, probe_points), probe_points, "", "", ""
|
| 85 |
if not text.strip():
|
| 86 |
-
|
|
|
|
| 87 |
if not _MODEL_READY or REDUCER is None:
|
| 88 |
msg = _html.gate_html([0.2] * 5, VIZ["teacher_names"] or ["—"] * 5)
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
|
| 102 |
# ── CSS ───────────────────────────────────────────────────────────────────
|
|
@@ -294,7 +316,10 @@ div[data-testid="model3d"], .model3D-component {
|
|
| 294 |
# ── Layout ────────────────────────────────────────────────────────────────
|
| 295 |
with gr.Blocks(css=CSS, theme=gr.themes.Base(), title="One for All") as demo:
|
| 296 |
|
| 297 |
-
gr.HTML(_html.header_html(
|
|
|
|
|
|
|
|
|
|
| 298 |
probe_state = gr.State([])
|
| 299 |
|
| 300 |
with gr.Tabs():
|
|
@@ -335,7 +360,42 @@ with gr.Blocks(css=CSS, theme=gr.themes.Base(), title="One for All") as demo:
|
|
| 335 |
'font-family:monospace;">↑ new probe point will appear in soul space</div>'
|
| 336 |
)
|
| 337 |
|
| 338 |
-
# ── Tab 2:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
with gr.TabItem("Geometry"):
|
| 340 |
with gr.Row():
|
| 341 |
with gr.Column(scale=7):
|
|
@@ -371,7 +431,7 @@ with gr.Blocks(css=CSS, theme=gr.themes.Base(), title="One for All") as demo:
|
|
| 371 |
f'</div>'
|
| 372 |
)
|
| 373 |
|
| 374 |
-
# ── Tab
|
| 375 |
with gr.TabItem("Training"):
|
| 376 |
with gr.Row():
|
| 377 |
gr.Plot(
|
|
@@ -389,6 +449,11 @@ with gr.Blocks(css=CSS, theme=gr.themes.Base(), title="One for All") as demo:
|
|
| 389 |
inputs=[prompt_box, probe_state],
|
| 390 |
outputs=[umap_plot, probe_state, resp_out, gate_out, task_out],
|
| 391 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
|
| 393 |
|
| 394 |
if __name__ == "__main__":
|
|
|
|
| 10 |
import gradio as gr
|
| 11 |
import spaces
|
| 12 |
|
|
|
|
| 13 |
import _fig
|
| 14 |
import _glb
|
| 15 |
import _html
|
| 16 |
+
|
| 17 |
+
# ── Startup: shared runtime (viz + UMAP + student) lives in _boot ─────────
|
| 18 |
+
import _boot
|
| 19 |
+
|
| 20 |
+
RT = _boot.load_runtime()
|
| 21 |
+
HF_TOKEN = RT.hf_token
|
| 22 |
+
VIZ = RT.viz
|
| 23 |
+
REDUCER = RT.reducer
|
| 24 |
+
COORDS3D = RT.coords3d
|
| 25 |
+
BACKEND = RT.backend
|
| 26 |
+
_MODEL_READY = RT.model_ready
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
_INIT_GLB = _glb.build_glb(VIZ, COORDS3D, [])
|
| 29 |
print(f"[ofa-space] GLB path: {_INIT_GLB}")
|
|
|
|
| 39 |
_CAM = (45, 30, 10)
|
| 40 |
|
| 41 |
|
| 42 |
+
def _response_html(text: str, title: str = "MODEL RESPONSE", accent: str = "#8b949e") -> str:
|
| 43 |
safe = _html_stdlib.escape(text).replace("\n", "<br>")
|
| 44 |
return (
|
| 45 |
'<div style="background:#0d1117;border:1px solid #30363d;border-radius:6px;'
|
| 46 |
'padding:14px;margin-top:8px;">'
|
| 47 |
+
f'<div style="font-size:10px;color:{accent};font-family:monospace;'
|
| 48 |
+
f'margin-bottom:8px;letter-spacing:0.04em;">{title}</div>'
|
| 49 |
f'<div style="font-size:13px;color:#e6edf3;line-height:1.65;">{safe}</div>'
|
| 50 |
"</div>"
|
| 51 |
)
|
| 52 |
|
| 53 |
|
| 54 |
+
# ── Backend dispatch: same handler code drives torch and llama.cpp ────────
|
| 55 |
+
def _to_device():
|
| 56 |
+
_boot.to_device(RT)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _stream(text: str):
|
| 60 |
+
return _boot.stream(RT, text)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _stream_pair(text: str):
|
| 64 |
+
return _boot.stream_pair(RT, text)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _final_probe(text: str):
|
| 68 |
+
return _boot.final_probe(RT, text)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
# ── ZeroGPU probe handler (streaming generator) ───────────────────────────
|
| 72 |
@spaces.GPU
|
| 73 |
+
def probe_fn(text: str, probe_points: list):
|
|
|
|
| 74 |
if not text.strip():
|
| 75 |
+
yield gr.skip(), probe_points, "", "", ""
|
| 76 |
+
return
|
| 77 |
if not _MODEL_READY or REDUCER is None:
|
| 78 |
msg = _html.gate_html([0.2] * 5, VIZ["teacher_names"] or ["—"] * 5)
|
| 79 |
+
yield _glb.build_glb(VIZ, COORDS3D, probe_points), probe_points, "", msg, ""
|
| 80 |
+
return
|
| 81 |
+
_to_device()
|
| 82 |
+
names = VIZ["teacher_names"]
|
| 83 |
+
|
| 84 |
+
# Stream tokens + live gate bars; skip the 3D plot until the end.
|
| 85 |
+
partial = ""
|
| 86 |
+
for partial, gates in _stream(text):
|
| 87 |
+
yield (
|
| 88 |
+
gr.skip(), probe_points,
|
| 89 |
+
_response_html(partial),
|
| 90 |
+
_html.gate_html(gates, names, ranked=False),
|
| 91 |
+
gr.skip(),
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
# Final pass: pooled probe point in soul space + dominant-teacher badge.
|
| 95 |
+
new_pt, gate_weights = _final_probe(text)
|
| 96 |
+
updated = probe_points + [new_pt]
|
| 97 |
+
yield (
|
| 98 |
+
_glb.build_glb(VIZ, COORDS3D, updated), updated,
|
| 99 |
+
_response_html(partial),
|
| 100 |
+
_html.gate_html(gate_weights, names),
|
| 101 |
+
_html.task_html(gate_weights, names),
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
# ── ZeroGPU arena handler: base (LoRA off) vs deku, same prompt ───────────
|
| 106 |
+
@spaces.GPU
|
| 107 |
+
def arena_fn(text: str):
|
| 108 |
+
if not text.strip():
|
| 109 |
+
yield "", "", ""
|
| 110 |
+
return
|
| 111 |
+
if not _MODEL_READY:
|
| 112 |
+
yield "", _response_html("model not loaded", "DEKU · DISTILLED"), ""
|
| 113 |
+
return
|
| 114 |
+
_to_device()
|
| 115 |
+
names = VIZ["teacher_names"]
|
| 116 |
+
for base_text, deku_text, gates in _stream_pair(text):
|
| 117 |
+
yield (
|
| 118 |
+
_response_html(base_text, "BASE · QWEN2.5-0.5B", accent="#8b949e"),
|
| 119 |
+
_response_html(deku_text, "DEKU · DISTILLED", accent="#7c3aed"),
|
| 120 |
+
_html.gate_html(gates, names, ranked=False),
|
| 121 |
+
)
|
| 122 |
|
| 123 |
|
| 124 |
# ── CSS ───────────────────────────────────────────────────────────────────
|
|
|
|
| 316 |
# ── Layout ────────────────────────────────────────────────────────────────
|
| 317 |
with gr.Blocks(css=CSS, theme=gr.themes.Base(), title="One for All") as demo:
|
| 318 |
|
| 319 |
+
gr.HTML(_html.header_html(
|
| 320 |
+
n_teachers=len(VIZ["teacher_names"]) or 6,
|
| 321 |
+
backend=BACKEND,
|
| 322 |
+
))
|
| 323 |
probe_state = gr.State([])
|
| 324 |
|
| 325 |
with gr.Tabs():
|
|
|
|
| 360 |
'font-family:monospace;">↑ new probe point will appear in soul space</div>'
|
| 361 |
)
|
| 362 |
|
| 363 |
+
# ── Tab 2: Arena — base vs deku, same prompt ──────────────────────
|
| 364 |
+
with gr.TabItem("Arena"):
|
| 365 |
+
gr.HTML(
|
| 366 |
+
'<div style="display:flex;align-items:center;gap:8px;'
|
| 367 |
+
'font-size:14px;font-weight:600;color:#e6edf3;margin:8px 0;">'
|
| 368 |
+
'<span style="color:#f59e0b;">⚔</span>One prompt, two models'
|
| 369 |
+
'<span style="font-family:monospace;font-size:10px;color:#8b949e;">'
|
| 370 |
+
'same 0.5B weights — LoRA adapter off vs on</span>'
|
| 371 |
+
'</div>'
|
| 372 |
+
)
|
| 373 |
+
arena_box = gr.Textbox(
|
| 374 |
+
lines=3,
|
| 375 |
+
placeholder="Ask something — watch base and distilled race side by side…",
|
| 376 |
+
label="",
|
| 377 |
+
)
|
| 378 |
+
arena_btn = gr.Button("Run both", variant="primary")
|
| 379 |
+
with gr.Row():
|
| 380 |
+
with gr.Column():
|
| 381 |
+
base_out = gr.HTML()
|
| 382 |
+
with gr.Column():
|
| 383 |
+
deku_out = gr.HTML()
|
| 384 |
+
arena_gate_out = gr.HTML()
|
| 385 |
+
gr.Examples(
|
| 386 |
+
examples=[
|
| 387 |
+
"Natalia sold clips to 48 of her friends in April, and then she "
|
| 388 |
+
"sold half as many clips in May. How many clips did Natalia sell "
|
| 389 |
+
"altogether in April and May?",
|
| 390 |
+
"Which property of a mineral can be determined just by looking "
|
| 391 |
+
"at it? (A) luster (B) mass (C) weight (D) hardness",
|
| 392 |
+
"Write a Python function that checks if a word is a palindrome.",
|
| 393 |
+
"Explain why the sky is blue in two sentences.",
|
| 394 |
+
],
|
| 395 |
+
inputs=[arena_box],
|
| 396 |
+
)
|
| 397 |
+
|
| 398 |
+
# ── Tab 3: Geometria ──────────────────────────────────────────────
|
| 399 |
with gr.TabItem("Geometry"):
|
| 400 |
with gr.Row():
|
| 401 |
with gr.Column(scale=7):
|
|
|
|
| 431 |
f'</div>'
|
| 432 |
)
|
| 433 |
|
| 434 |
+
# ── Tab 4: Treino ─────────────────────────────────────────────────
|
| 435 |
with gr.TabItem("Training"):
|
| 436 |
with gr.Row():
|
| 437 |
gr.Plot(
|
|
|
|
| 449 |
inputs=[prompt_box, probe_state],
|
| 450 |
outputs=[umap_plot, probe_state, resp_out, gate_out, task_out],
|
| 451 |
)
|
| 452 |
+
arena_btn.click(
|
| 453 |
+
arena_fn,
|
| 454 |
+
inputs=[arena_box],
|
| 455 |
+
outputs=[base_out, deku_out, arena_gate_out],
|
| 456 |
+
)
|
| 457 |
|
| 458 |
|
| 459 |
if __name__ == "__main__":
|
frontend/app.js
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* One for All — custom frontend wired to the gr.Server backend.
|
| 2 |
+
* Visual design is unchanged from the static mock; the mock answer engine and
|
| 3 |
+
* the random thought-map are replaced by real calls to /probe, /arena, /viz
|
| 4 |
+
* via @gradio/client (browser-side calls are required for ZeroGPU). */
|
| 5 |
+
import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/+esm";
|
| 6 |
+
|
| 7 |
+
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
| 8 |
+
const cssv = (v) => getComputedStyle(document.documentElement).getPropertyValue(v).trim();
|
| 9 |
+
|
| 10 |
+
// ── teacher lineup (order matches the backend gate vector) ──
|
| 11 |
+
const TEACHERS = [
|
| 12 |
+
{ id: "qwen", label: "qwen", varc: "--t-qwen" },
|
| 13 |
+
{ id: "smollm", label: "smollm", varc: "--t-smollm" },
|
| 14 |
+
{ id: "phi", label: "phi", varc: "--t-phi" },
|
| 15 |
+
{ id: "gemma", label: "gemma", varc: "--t-gemma" },
|
| 16 |
+
{ id: "minicpm", label: "minicpm", varc: "--t-minicpm" },
|
| 17 |
+
{ id: "nemotron", label: "nemotron", varc: "--t-nemo" },
|
| 18 |
+
];
|
| 19 |
+
const colorFor = (name) =>
|
| 20 |
+
name === "student" ? cssv("--spark")
|
| 21 |
+
: cssv((TEACHERS.find((t) => t.id === name) || {}).varc || "--text-dim");
|
| 22 |
+
|
| 23 |
+
// ── 1. CONVERGENCE DIAGRAM (unchanged) ──────────────────────────────────
|
| 24 |
+
(function buildConverge() {
|
| 25 |
+
const cx = 300, cy = 300, R = 232;
|
| 26 |
+
const threads = document.getElementById("threads");
|
| 27 |
+
const flows = document.getElementById("flows");
|
| 28 |
+
const tnodes = document.getElementById("tnodes");
|
| 29 |
+
TEACHERS.forEach((t, i) => {
|
| 30 |
+
const a = (-90 + i * 60) * Math.PI / 180;
|
| 31 |
+
const x = cx + R * Math.cos(a), y = cy + R * Math.sin(a);
|
| 32 |
+
const mx = (x + cx) / 2, my = (y + cy) / 2;
|
| 33 |
+
const perp = a + Math.PI / 2;
|
| 34 |
+
const ctrlX = mx + 46 * Math.cos(perp), ctrlY = my + 46 * Math.sin(perp);
|
| 35 |
+
const d = `M ${x} ${y} Q ${ctrlX} ${ctrlY} ${cx} ${cy}`;
|
| 36 |
+
const col = cssv(t.varc);
|
| 37 |
+
|
| 38 |
+
const base = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
| 39 |
+
base.setAttribute("d", d); base.setAttribute("class", "thread");
|
| 40 |
+
base.setAttribute("stroke", col);
|
| 41 |
+
threads.appendChild(base);
|
| 42 |
+
|
| 43 |
+
const flow = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
| 44 |
+
flow.setAttribute("d", d); flow.setAttribute("class", "thread-flow");
|
| 45 |
+
flow.setAttribute("stroke", col);
|
| 46 |
+
if (!reduce) flow.style.animation = `flow ${2.4 + i * 0.25}s linear infinite`;
|
| 47 |
+
flows.appendChild(flow);
|
| 48 |
+
|
| 49 |
+
const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
| 50 |
+
const halo = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
| 51 |
+
halo.setAttribute("cx", x); halo.setAttribute("cy", y); halo.setAttribute("r", 13);
|
| 52 |
+
halo.setAttribute("fill", col); halo.setAttribute("opacity", "0.22"); halo.setAttribute("filter", "url(#soft)");
|
| 53 |
+
const node = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
| 54 |
+
node.setAttribute("cx", x); node.setAttribute("cy", y); node.setAttribute("r", 7);
|
| 55 |
+
node.setAttribute("fill", col); node.setAttribute("class", "tnode");
|
| 56 |
+
const lx = cx + (R + 30) * Math.cos(a), ly = cy + (R + 30) * Math.sin(a);
|
| 57 |
+
const isRight = Math.cos(a) > 0.2, isLeft = Math.cos(a) < -0.2;
|
| 58 |
+
const lbl = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
| 59 |
+
lbl.setAttribute("x", lx); lbl.setAttribute("y", ly + 4);
|
| 60 |
+
lbl.setAttribute("text-anchor", isRight ? "start" : isLeft ? "end" : "middle");
|
| 61 |
+
lbl.setAttribute("class", "tlabel"); lbl.textContent = t.label;
|
| 62 |
+
g.appendChild(halo); g.appendChild(node); g.appendChild(lbl);
|
| 63 |
+
tnodes.appendChild(g);
|
| 64 |
+
});
|
| 65 |
+
})();
|
| 66 |
+
|
| 67 |
+
// ── 2. EXAMPLES + INFLUENCE ROWS (unchanged scaffold) ───────────────────
|
| 68 |
+
const EXAMPLES = [
|
| 69 |
+
"Natalia sold clips to 48 friends in April, then half as many in May. How many altogether?",
|
| 70 |
+
"Which property of a mineral can be determined just by looking at it? (A) luster (B) mass (C) weight (D) hardness",
|
| 71 |
+
"Write a Python function that checks if a word is a palindrome.",
|
| 72 |
+
"Explain why the sky is blue in two sentences.",
|
| 73 |
+
];
|
| 74 |
+
const exWrap = document.getElementById("examples");
|
| 75 |
+
EXAMPLES.forEach((ex, i) => {
|
| 76 |
+
const li = document.createElement("li");
|
| 77 |
+
const b = document.createElement("button");
|
| 78 |
+
b.className = "chip"; b.type = "button";
|
| 79 |
+
b.innerHTML = `<span class="ix">0${i + 1}</span><span class="qt"></span><span class="go">↵</span>`;
|
| 80 |
+
b.querySelector(".qt").textContent = ex;
|
| 81 |
+
b.addEventListener("click", () => {
|
| 82 |
+
document.getElementById("ask").value = ex;
|
| 83 |
+
document.getElementById("ask").focus();
|
| 84 |
+
});
|
| 85 |
+
li.appendChild(b); exWrap.appendChild(li);
|
| 86 |
+
});
|
| 87 |
+
|
| 88 |
+
const infRows = document.getElementById("infRows");
|
| 89 |
+
const fills = {};
|
| 90 |
+
TEACHERS.forEach((t) => {
|
| 91 |
+
const row = document.createElement("div"); row.className = "inf-row";
|
| 92 |
+
row.style.color = cssv(t.varc);
|
| 93 |
+
row.innerHTML = `<span class="nm">${t.label}</span>
|
| 94 |
+
<span class="track"><span class="fill"></span></span>
|
| 95 |
+
<span class="pc">—</span>`;
|
| 96 |
+
infRows.appendChild(row);
|
| 97 |
+
fills[t.id] = { fill: row.querySelector(".fill"), pc: row.querySelector(".pc") };
|
| 98 |
+
});
|
| 99 |
+
|
| 100 |
+
function setInfluence(gates) {
|
| 101 |
+
if (!gates) return;
|
| 102 |
+
TEACHERS.forEach((t, i) => {
|
| 103 |
+
const val = gates[i] || 0;
|
| 104 |
+
fills[t.id].fill.style.width = (val * 100).toFixed(0) + "%";
|
| 105 |
+
fills[t.id].pc.textContent = val > 0 ? val.toFixed(2).slice(1) : "—";
|
| 106 |
+
});
|
| 107 |
+
}
|
| 108 |
+
function resetInfluence() {
|
| 109 |
+
TEACHERS.forEach((t) => { fills[t.id].fill.style.width = "0%"; fills[t.id].pc.textContent = "—"; });
|
| 110 |
+
}
|
| 111 |
+
function topIdx(g) { let m = 0; for (let i = 1; i < g.length; i++) if (g[i] > g[m]) m = i; return m; }
|
| 112 |
+
|
| 113 |
+
// ── 3. BACKEND-WIRED ASK / COMPARE ──────────────────────────────────────
|
| 114 |
+
let mode = "ask";
|
| 115 |
+
let busy = false;
|
| 116 |
+
let client = null;
|
| 117 |
+
const answers = document.getElementById("answers");
|
| 118 |
+
const beforeCard = document.getElementById("beforeCard");
|
| 119 |
+
const bodyAfter = document.getElementById("bodyAfter");
|
| 120 |
+
const bodyBefore = document.getElementById("bodyBefore");
|
| 121 |
+
const askBtn = document.getElementById("askBtn");
|
| 122 |
+
const infNote = document.getElementById("infNote");
|
| 123 |
+
|
| 124 |
+
document.querySelectorAll(".seg button").forEach((b) => {
|
| 125 |
+
b.addEventListener("click", () => {
|
| 126 |
+
document.querySelectorAll(".seg button").forEach((x) => x.setAttribute("aria-pressed", "false"));
|
| 127 |
+
b.setAttribute("aria-pressed", "true");
|
| 128 |
+
mode = b.dataset.mode;
|
| 129 |
+
answers.classList.toggle("compare", mode === "compare");
|
| 130 |
+
beforeCard.hidden = (mode !== "compare");
|
| 131 |
+
});
|
| 132 |
+
});
|
| 133 |
+
|
| 134 |
+
function render(el, text, streaming) {
|
| 135 |
+
el.textContent = text || "";
|
| 136 |
+
if (streaming) {
|
| 137 |
+
const caret = document.createElement("span");
|
| 138 |
+
caret.className = "caret"; caret.textContent = "▋";
|
| 139 |
+
el.appendChild(caret);
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
async function ask() {
|
| 144 |
+
const text = document.getElementById("ask").value.trim();
|
| 145 |
+
if (!text || busy) return;
|
| 146 |
+
if (!client) { render(bodyAfter, "⚠ backend not connected", false); return; }
|
| 147 |
+
|
| 148 |
+
busy = true;
|
| 149 |
+
askBtn.classList.add("busy"); askBtn.firstChild.textContent = "Thinking ";
|
| 150 |
+
infNote.textContent = "routing through teachers…";
|
| 151 |
+
resetInfluence();
|
| 152 |
+
bodyAfter.innerHTML = '<span class="placeholder-note">distilling…</span>';
|
| 153 |
+
if (mode === "compare") bodyBefore.textContent = "";
|
| 154 |
+
|
| 155 |
+
try {
|
| 156 |
+
let gates = null;
|
| 157 |
+
if (mode === "compare") {
|
| 158 |
+
const job = client.submit("/arena", { text });
|
| 159 |
+
for await (const msg of job) {
|
| 160 |
+
if (msg.type !== "data" || !msg.data?.[0]) continue;
|
| 161 |
+
const d = msg.data[0];
|
| 162 |
+
if (d.error) { render(bodyAfter, "⚠ " + d.error, false); break; }
|
| 163 |
+
gates = d.gates; setInfluence(gates);
|
| 164 |
+
render(bodyBefore, d.base, !d.done);
|
| 165 |
+
render(bodyAfter, d.deku, !d.done);
|
| 166 |
+
if (d.done && gates) dropProbeByGates(gates);
|
| 167 |
+
}
|
| 168 |
+
} else {
|
| 169 |
+
const job = client.submit("/probe", { text });
|
| 170 |
+
for await (const msg of job) {
|
| 171 |
+
if (msg.type !== "data" || !msg.data?.[0]) continue;
|
| 172 |
+
const d = msg.data[0];
|
| 173 |
+
if (d.error) { render(bodyAfter, "⚠ " + d.error, false); break; }
|
| 174 |
+
gates = d.gates; setInfluence(gates);
|
| 175 |
+
render(bodyAfter, d.text, !d.done);
|
| 176 |
+
if (d.done) { if (d.point) dropProbePoint(d.point); else if (gates) dropProbeByGates(gates); }
|
| 177 |
+
}
|
| 178 |
+
}
|
| 179 |
+
if (gates) {
|
| 180 |
+
const t = TEACHERS[topIdx(gates)].label;
|
| 181 |
+
infNote.textContent = `leaning on ${t} · ${(gates[topIdx(gates)] * 100).toFixed(0)}%`;
|
| 182 |
+
} else {
|
| 183 |
+
infNote.textContent = "idle — waiting for a prompt";
|
| 184 |
+
}
|
| 185 |
+
} catch (e) {
|
| 186 |
+
console.error(e);
|
| 187 |
+
render(bodyAfter, "⚠ " + (e.message || e), false);
|
| 188 |
+
infNote.textContent = "error";
|
| 189 |
+
} finally {
|
| 190 |
+
busy = false;
|
| 191 |
+
askBtn.classList.remove("busy"); askBtn.firstChild.textContent = "Ask ";
|
| 192 |
+
}
|
| 193 |
+
}
|
| 194 |
+
askBtn.addEventListener("click", ask);
|
| 195 |
+
document.getElementById("ask").addEventListener("keydown", (e) => {
|
| 196 |
+
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") ask();
|
| 197 |
+
});
|
| 198 |
+
|
| 199 |
+
// ── 4. THOUGHT MAP (real coords from /viz, procedural fallback offline) ──
|
| 200 |
+
const stage = document.getElementById("mapStage");
|
| 201 |
+
const layers = {
|
| 202 |
+
far: document.getElementById("mapFar"),
|
| 203 |
+
mid: document.getElementById("mapMid"),
|
| 204 |
+
near: document.getElementById("mapNear"),
|
| 205 |
+
};
|
| 206 |
+
const legend = document.getElementById("mapLegend");
|
| 207 |
+
|
| 208 |
+
let VIZ = null;
|
| 209 |
+
let PROJ = null; // {minX,maxX,minY,maxY} bbox of all model points
|
| 210 |
+
let CENTROIDS = {}; // teacher id → [nx, ny] in 0..1, for gate-based probe drop
|
| 211 |
+
|
| 212 |
+
// fallback cluster centers (used only when the backend has no coords)
|
| 213 |
+
const CENTERS = {
|
| 214 |
+
qwen: [0.30, 0.32], smollm: [0.70, 0.26], phi: [0.80, 0.58],
|
| 215 |
+
gemma: [0.58, 0.78], minicpm: [0.30, 0.72], nemotron: [0.46, 0.50],
|
| 216 |
+
};
|
| 217 |
+
const rnd = (n) => (Math.random() - 0.5) * n;
|
| 218 |
+
|
| 219 |
+
function clearLayers() { Object.values(layers).forEach((l) => (l.innerHTML = "")); }
|
| 220 |
+
|
| 221 |
+
function addDot(layer, leftPct, topPct, size, color, opacity) {
|
| 222 |
+
const d = document.createElement("div"); d.className = "dot";
|
| 223 |
+
d.style.left = leftPct + "%"; d.style.top = topPct + "%";
|
| 224 |
+
d.style.width = d.style.height = size + "px";
|
| 225 |
+
d.style.color = color; d.style.background = color; d.style.opacity = opacity;
|
| 226 |
+
layer.appendChild(d);
|
| 227 |
+
return d;
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
function computeProjection(models) {
|
| 231 |
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
| 232 |
+
for (const m of models) for (const p of m.points) {
|
| 233 |
+
if (p[0] < minX) minX = p[0]; if (p[0] > maxX) maxX = p[0];
|
| 234 |
+
if (p[1] < minY) minY = p[1]; if (p[1] > maxY) maxY = p[1];
|
| 235 |
+
}
|
| 236 |
+
return { minX, maxX, minY, maxY };
|
| 237 |
+
}
|
| 238 |
+
function project(p) { // 3D point → [leftPct, topPct] with 8% padding
|
| 239 |
+
const { minX, maxX, minY, maxY } = PROJ;
|
| 240 |
+
const nx = (p[0] - minX) / ((maxX - minX) || 1);
|
| 241 |
+
const ny = (p[1] - minY) / ((maxY - minY) || 1);
|
| 242 |
+
return [
|
| 243 |
+
Math.max(0, Math.min(1, 0.08 + nx * 0.84)) * 100,
|
| 244 |
+
Math.max(0, Math.min(1, 0.08 + ny * 0.84)) * 100,
|
| 245 |
+
];
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
function placeReal() {
|
| 249 |
+
clearLayers();
|
| 250 |
+
PROJ = computeProjection(VIZ.models);
|
| 251 |
+
CENTROIDS = {};
|
| 252 |
+
for (const m of VIZ.models) {
|
| 253 |
+
const col = colorFor(m.name);
|
| 254 |
+
const isStudent = m.name === "student";
|
| 255 |
+
let sx = 0, sy = 0;
|
| 256 |
+
m.points.forEach((p, k) => {
|
| 257 |
+
const [lp, tp] = project(p);
|
| 258 |
+
sx += lp; sy += tp;
|
| 259 |
+
const depth = isStudent ? "near" : (k % 3 === 0 ? "far" : k % 3 === 1 ? "mid" : "near");
|
| 260 |
+
const sz = isStudent ? 7 : depth === "near" ? 6 : depth === "mid" ? 4.5 : 3;
|
| 261 |
+
const op = isStudent ? 1 : depth === "near" ? 1 : depth === "mid" ? 0.8 : 0.55;
|
| 262 |
+
addDot(layers[depth], lp, tp, sz, col, op);
|
| 263 |
+
});
|
| 264 |
+
if (!isStudent && m.points.length) CENTROIDS[m.name] = [sx / m.points.length / 100, sy / m.points.length / 100];
|
| 265 |
+
}
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
function placeFallback() {
|
| 269 |
+
clearLayers();
|
| 270 |
+
PROJ = null;
|
| 271 |
+
CENTROIDS = {};
|
| 272 |
+
TEACHERS.forEach((t) => {
|
| 273 |
+
const c = CENTERS[t.id]; const col = cssv(t.varc);
|
| 274 |
+
CENTROIDS[t.id] = c;
|
| 275 |
+
for (let k = 0; k < 11; k++) {
|
| 276 |
+
const depth = k % 3 === 0 ? "far" : k % 3 === 1 ? "mid" : "near";
|
| 277 |
+
const sz = depth === "near" ? 6 : depth === "mid" ? 4.5 : 3;
|
| 278 |
+
const op = depth === "near" ? 1 : depth === "mid" ? 0.8 : 0.55;
|
| 279 |
+
addDot(layers[depth], (c[0] + rnd(0.13)) * 100, (c[1] + rnd(0.13)) * 100, sz, col, op);
|
| 280 |
+
}
|
| 281 |
+
});
|
| 282 |
+
for (let k = 0; k < 9; k++) {
|
| 283 |
+
addDot(layers.near, (0.5 + rnd(0.22)) * 100, (0.52 + rnd(0.2)) * 100, 7, cssv("--spark"), 1);
|
| 284 |
+
}
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
function placeMap() {
|
| 288 |
+
if (VIZ && VIZ.models && VIZ.models.length) placeReal();
|
| 289 |
+
else placeFallback();
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
function dropProbePoint(point) {
|
| 293 |
+
let lp, tp;
|
| 294 |
+
if (PROJ) { [lp, tp] = project([point.x, point.y, point.z]); }
|
| 295 |
+
else { lp = 50 + rnd(20) * 2; tp = 52 + rnd(18) * 2; }
|
| 296 |
+
const d = addDot(layers.near, lp, tp, 9, cssv("--text"), 1);
|
| 297 |
+
d.classList.add("probe");
|
| 298 |
+
}
|
| 299 |
+
function dropProbeByGates(gates) {
|
| 300 |
+
const id = TEACHERS[topIdx(gates)].id;
|
| 301 |
+
const c = CENTROIDS[id] || [0.5, 0.5];
|
| 302 |
+
const lp = (c[0] * 0.6 + 0.5 * 0.4 + rnd(0.06)) * 100;
|
| 303 |
+
const tp = (c[1] * 0.6 + 0.52 * 0.4 + rnd(0.06)) * 100;
|
| 304 |
+
const d = addDot(layers.near, lp, tp, 9, cssv("--text"), 1);
|
| 305 |
+
d.classList.add("probe");
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
function buildLegend() {
|
| 309 |
+
legend.innerHTML = "";
|
| 310 |
+
[{ label: "student", c: "--spark" }, ...TEACHERS.map((t) => ({ label: t.label, c: t.varc })), { label: "probe", c: "--text" }]
|
| 311 |
+
.forEach((it) => {
|
| 312 |
+
const s = document.createElement("div"); s.className = "lg"; s.style.color = cssv(it.c);
|
| 313 |
+
s.innerHTML = `<span class="s"></span><span style="color:var(--text-dim)">${it.label}</span>`;
|
| 314 |
+
legend.appendChild(s);
|
| 315 |
+
});
|
| 316 |
+
}
|
| 317 |
+
buildLegend();
|
| 318 |
+
placeMap();
|
| 319 |
+
window.addEventListener("resize", () => { clearTimeout(window.__rt); window.__rt = setTimeout(placeMap, 250); });
|
| 320 |
+
|
| 321 |
+
// ── 5. PARALLAX (unchanged) ─────────────────────────────────────────────
|
| 322 |
+
const ambient = document.getElementById("ambient");
|
| 323 |
+
const converge = document.getElementById("converge");
|
| 324 |
+
let sy = 0, ticking = false;
|
| 325 |
+
function onScroll() { sy = window.scrollY || 0; if (!ticking) { requestAnimationFrame(applyScroll); ticking = true; } }
|
| 326 |
+
function applyScroll() {
|
| 327 |
+
if (!reduce) {
|
| 328 |
+
ambient.style.transform = `translate3d(0, ${sy * 0.18}px, 0)`;
|
| 329 |
+
converge.style.transform = `translate3d(0, ${sy * -0.04}px, 0)`;
|
| 330 |
+
}
|
| 331 |
+
ticking = false;
|
| 332 |
+
}
|
| 333 |
+
window.addEventListener("scroll", onScroll, { passive: true });
|
| 334 |
+
|
| 335 |
+
let mx = 0, my = 0, cmx = 0, cmy = 0;
|
| 336 |
+
document.addEventListener("mousemove", (e) => {
|
| 337 |
+
mx = (e.clientX / window.innerWidth - 0.5);
|
| 338 |
+
my = (e.clientY / window.innerHeight - 0.5);
|
| 339 |
+
});
|
| 340 |
+
function rafMouse() {
|
| 341 |
+
cmx += (mx - cmx) * 0.06; cmy += (my - cmy) * 0.06;
|
| 342 |
+
if (!reduce) {
|
| 343 |
+
const tn = document.getElementById("tnodes");
|
| 344 |
+
const fl = document.getElementById("flows");
|
| 345 |
+
if (tn) tn.style.transform = `translate(${cmx * 22}px, ${cmy * 22}px)`;
|
| 346 |
+
if (fl) fl.style.transform = `translate(${cmx * 10}px, ${cmy * 10}px)`;
|
| 347 |
+
const r = stage.getBoundingClientRect();
|
| 348 |
+
if (r.top < window.innerHeight && r.bottom > 0) {
|
| 349 |
+
Object.values(layers).forEach((l) => {
|
| 350 |
+
const dep = parseFloat(l.dataset.depth) * 1;
|
| 351 |
+
l.style.transform = `translate(${cmx * dep * 900}px, ${cmy * dep * 700}px)`;
|
| 352 |
+
});
|
| 353 |
+
}
|
| 354 |
+
}
|
| 355 |
+
requestAnimationFrame(rafMouse);
|
| 356 |
+
}
|
| 357 |
+
requestAnimationFrame(rafMouse);
|
| 358 |
+
|
| 359 |
+
// ── 6. SCROLL REVEAL (unchanged) ────────────────────────────────────────
|
| 360 |
+
const io = new IntersectionObserver((ents) => {
|
| 361 |
+
ents.forEach((en) => { if (en.isIntersecting) { en.target.classList.add("in"); io.unobserve(en.target); } });
|
| 362 |
+
}, { threshold: 0.18 });
|
| 363 |
+
document.querySelectorAll(".reveal").forEach((el) => io.observe(el));
|
| 364 |
+
|
| 365 |
+
// ── 7. smooth scroll buttons (unchanged) ────────────────────────────────
|
| 366 |
+
document.querySelectorAll("[data-scroll]").forEach((b) => {
|
| 367 |
+
b.addEventListener("click", () => {
|
| 368 |
+
const el = document.querySelector(b.dataset.scroll);
|
| 369 |
+
if (el) el.scrollIntoView({ behavior: reduce ? "auto" : "smooth", block: "start" });
|
| 370 |
+
});
|
| 371 |
+
});
|
| 372 |
+
|
| 373 |
+
// ── 0. connect + load viz ───────────────────────────────────────────────
|
| 374 |
+
(async () => {
|
| 375 |
+
try {
|
| 376 |
+
client = await Client.connect(location.origin);
|
| 377 |
+
const res = await client.predict("/viz", {});
|
| 378 |
+
VIZ = res.data[0];
|
| 379 |
+
const n = (VIZ.teachers && VIZ.teachers.length) || 6;
|
| 380 |
+
document.getElementById("statTeachers").innerHTML =
|
| 381 |
+
`<span class="d t"></span>${n} Teachers <b>· 1.5–4B</b>`;
|
| 382 |
+
if (!VIZ.model_ready) infNote.textContent = "model offline — UI preview only";
|
| 383 |
+
placeMap();
|
| 384 |
+
} catch (e) {
|
| 385 |
+
console.error("boot failed", e);
|
| 386 |
+
infNote.textContent = "backend unreachable — UI preview only";
|
| 387 |
+
}
|
| 388 |
+
})();
|
frontend/index.html
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>One for All — the strength of many, carried by one</title>
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 8 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 9 |
+
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Hanken+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
| 10 |
+
<style>
|
| 11 |
+
:root {
|
| 12 |
+
/* ── ink base (deep, cool, near-black) ── */
|
| 13 |
+
--ink-0: oklch(0.155 0.012 264);
|
| 14 |
+
--ink-1: oklch(0.195 0.013 264);
|
| 15 |
+
--ink-2: oklch(0.235 0.014 264);
|
| 16 |
+
--line: oklch(0.42 0.02 264 / 0.42);
|
| 17 |
+
--line-soft: oklch(0.42 0.02 264 / 0.20);
|
| 18 |
+
|
| 19 |
+
--text: oklch(0.955 0.008 85);
|
| 20 |
+
--text-dim: oklch(0.72 0.012 264);
|
| 21 |
+
--text-faint: oklch(0.56 0.012 264);
|
| 22 |
+
|
| 23 |
+
/* ── the one warm accent (inherited light / the student) ── */
|
| 24 |
+
--spark: oklch(0.83 0.125 74);
|
| 25 |
+
--spark-deep: oklch(0.74 0.135 66);
|
| 26 |
+
--spark-soft: oklch(0.83 0.125 74 / 0.13);
|
| 27 |
+
--spark-line: oklch(0.83 0.125 74 / 0.30);
|
| 28 |
+
|
| 29 |
+
/* ── 6 teachers: one family, lightness+chroma fixed, hue varies ── */
|
| 30 |
+
--t-qwen: oklch(0.76 0.115 252);
|
| 31 |
+
--t-smollm: oklch(0.76 0.115 200);
|
| 32 |
+
--t-phi: oklch(0.76 0.115 156);
|
| 33 |
+
--t-gemma: oklch(0.76 0.115 122);
|
| 34 |
+
--t-minicpm: oklch(0.76 0.115 38);
|
| 35 |
+
--t-nemo: oklch(0.76 0.115 322);
|
| 36 |
+
|
| 37 |
+
--serif: "Instrument Serif", Georgia, "Times New Roman", serif;
|
| 38 |
+
--ui: "Hanken Grotesk", system-ui, -apple-system, "Segoe UI", sans-serif;
|
| 39 |
+
--mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
| 40 |
+
|
| 41 |
+
--maxw: 1180px;
|
| 42 |
+
--ease: cubic-bezier(0.22, 1, 0.36, 1);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
* { box-sizing: border-box; }
|
| 46 |
+
html { scroll-behavior: smooth; }
|
| 47 |
+
body {
|
| 48 |
+
margin: 0;
|
| 49 |
+
background: var(--ink-0);
|
| 50 |
+
color: var(--text);
|
| 51 |
+
font-family: var(--ui);
|
| 52 |
+
font-size: 16px;
|
| 53 |
+
line-height: 1.55;
|
| 54 |
+
-webkit-font-smoothing: antialiased;
|
| 55 |
+
text-rendering: optimizeLegibility;
|
| 56 |
+
overflow-x: hidden;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
/* layered ambient field — fixed, gets parallax via JS */
|
| 60 |
+
.ambient {
|
| 61 |
+
position: fixed; inset: -10% -10% -10% -10%;
|
| 62 |
+
z-index: 0; pointer-events: none;
|
| 63 |
+
background:
|
| 64 |
+
radial-gradient(42% 38% at 22% 14%, oklch(0.76 0.115 252 / 0.10), transparent 70%),
|
| 65 |
+
radial-gradient(40% 36% at 82% 8%, var(--spark-soft), transparent 72%),
|
| 66 |
+
radial-gradient(50% 46% at 60% 96%, oklch(0.76 0.115 322 / 0.07), transparent 74%);
|
| 67 |
+
will-change: transform;
|
| 68 |
+
}
|
| 69 |
+
.grain {
|
| 70 |
+
position: fixed; inset: 0; z-index: 1; pointer-events: none;
|
| 71 |
+
opacity: 0.035; mix-blend-mode: screen;
|
| 72 |
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
::-webkit-scrollbar { width: 11px; }
|
| 76 |
+
::-webkit-scrollbar-track { background: transparent; }
|
| 77 |
+
::-webkit-scrollbar-thumb { background: var(--ink-2); border-radius: 99px; border: 3px solid var(--ink-0); }
|
| 78 |
+
::-webkit-scrollbar-thumb:hover { background: var(--spark-deep); }
|
| 79 |
+
::selection { background: var(--spark-soft); color: var(--text); }
|
| 80 |
+
|
| 81 |
+
/* ── top bar ── */
|
| 82 |
+
.topbar {
|
| 83 |
+
position: sticky; top: 0; z-index: 40;
|
| 84 |
+
display: flex; align-items: center; justify-content: space-between;
|
| 85 |
+
padding: 16px clamp(20px, 4vw, 40px);
|
| 86 |
+
backdrop-filter: blur(14px) saturate(1.2);
|
| 87 |
+
background: linear-gradient(180deg, oklch(0.155 0.012 264 / 0.86), oklch(0.155 0.012 264 / 0.4));
|
| 88 |
+
border-bottom: 1px solid var(--line-soft);
|
| 89 |
+
}
|
| 90 |
+
.brand { display: flex; align-items: center; gap: 12px; }
|
| 91 |
+
.mark {
|
| 92 |
+
font-family: var(--serif); font-size: 30px; line-height: 1;
|
| 93 |
+
color: var(--spark);
|
| 94 |
+
text-shadow: 0 0 22px var(--spark-line);
|
| 95 |
+
transform: translateY(-1px);
|
| 96 |
+
}
|
| 97 |
+
.brand .name { font-weight: 600; letter-spacing: -0.01em; font-size: 16px; white-space: nowrap; }
|
| 98 |
+
.brand .name b { font-weight: 600; }
|
| 99 |
+
.stat-row { display: flex; align-items: center; gap: 18px; }
|
| 100 |
+
.stat {
|
| 101 |
+
font-family: var(--mono); font-size: 11px; letter-spacing: 0.06em;
|
| 102 |
+
color: var(--text-faint); display: flex; align-items: center; gap: 7px;
|
| 103 |
+
text-transform: uppercase;
|
| 104 |
+
}
|
| 105 |
+
.stat .d { width: 6px; height: 6px; border-radius: 50%; }
|
| 106 |
+
.stat .d.s { background: var(--spark); box-shadow: 0 0 8px var(--spark); }
|
| 107 |
+
.stat .d.t { background: var(--t-qwen); box-shadow: 0 0 8px var(--t-qwen); }
|
| 108 |
+
.stat b { color: var(--text-dim); font-weight: 500; }
|
| 109 |
+
@media (max-width: 760px){ .stat-row .stat:last-child { display: none; } }
|
| 110 |
+
|
| 111 |
+
/* ── shared layout ── */
|
| 112 |
+
.wrap { position: relative; z-index: 2; max-width: var(--maxw); margin: 0 auto; padding: 0 clamp(20px, 4vw, 40px); }
|
| 113 |
+
.eyebrow {
|
| 114 |
+
font-family: var(--mono); font-size: 11px; letter-spacing: 0.28em;
|
| 115 |
+
text-transform: uppercase; color: var(--text-faint);
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
/* ── HERO ── */
|
| 119 |
+
.hero { position: relative; padding: clamp(56px, 11vh, 130px) 0 clamp(40px, 7vh, 90px); }
|
| 120 |
+
.hero-grid {
|
| 121 |
+
display: grid; grid-template-columns: 1.05fr 0.95fr; gap: clamp(24px, 5vw, 64px);
|
| 122 |
+
align-items: center;
|
| 123 |
+
}
|
| 124 |
+
.hero h1 {
|
| 125 |
+
font-family: var(--serif); font-weight: 400;
|
| 126 |
+
font-size: clamp(46px, 7.6vw, 104px); line-height: 0.96;
|
| 127 |
+
letter-spacing: -0.015em; margin: 18px 0 0;
|
| 128 |
+
}
|
| 129 |
+
.hero h1 .em { font-style: italic; color: var(--spark); }
|
| 130 |
+
.hero .lede {
|
| 131 |
+
margin: 26px 0 0; max-width: 30em;
|
| 132 |
+
font-size: clamp(16px, 1.5vw, 19px); line-height: 1.62; color: var(--text-dim);
|
| 133 |
+
}
|
| 134 |
+
.hero .lede b { color: var(--text); font-weight: 600; }
|
| 135 |
+
.hero-cta { display: flex; align-items: center; gap: 18px; margin-top: 34px; flex-wrap: wrap; }
|
| 136 |
+
.btn {
|
| 137 |
+
appearance: none; border: 0; cursor: pointer; font-family: var(--ui);
|
| 138 |
+
font-weight: 600; font-size: 15px; letter-spacing: 0.005em;
|
| 139 |
+
padding: 13px 22px; border-radius: 99px;
|
| 140 |
+
display: inline-flex; align-items: center; gap: 10px;
|
| 141 |
+
transition: transform .25s var(--ease), box-shadow .25s var(--ease), background .2s;
|
| 142 |
+
}
|
| 143 |
+
.btn-spark {
|
| 144 |
+
color: oklch(0.2 0.04 70);
|
| 145 |
+
background: linear-gradient(180deg, var(--spark), var(--spark-deep));
|
| 146 |
+
box-shadow: 0 1px 0 oklch(1 0 0 / 0.3) inset, 0 10px 30px -10px var(--spark-deep);
|
| 147 |
+
}
|
| 148 |
+
.btn-spark:hover { transform: translateY(-2px); box-shadow: 0 1px 0 oklch(1 0 0 / 0.3) inset, 0 16px 36px -12px var(--spark-deep); }
|
| 149 |
+
.btn-ghost { color: var(--text-dim); background: transparent; border: 1px solid var(--line); }
|
| 150 |
+
.btn-ghost:hover { color: var(--text); border-color: var(--text-faint); }
|
| 151 |
+
.btn .arrow { font-family: var(--mono); }
|
| 152 |
+
|
| 153 |
+
/* convergence diagram */
|
| 154 |
+
.converge { position: relative; aspect-ratio: 1 / 1; width: 100%; }
|
| 155 |
+
.converge svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
|
| 156 |
+
.converge .tlabel {
|
| 157 |
+
font-family: var(--mono); font-size: 11px; letter-spacing: 0.04em; fill: var(--text-dim);
|
| 158 |
+
}
|
| 159 |
+
.thread { fill: none; stroke-width: 1.4; opacity: 0.5; }
|
| 160 |
+
.thread-flow { fill: none; stroke-width: 1.6; stroke-dasharray: 2 13; stroke-linecap: round; }
|
| 161 |
+
@keyframes flow { to { stroke-dashoffset: -150; } }
|
| 162 |
+
.core-ring { animation: spinSlow 38s linear infinite; transform-origin: 300px 300px; }
|
| 163 |
+
@keyframes spinSlow { to { transform: rotate(360deg); } }
|
| 164 |
+
@keyframes breathe { 0%,100% { opacity: .55; r: 30; } 50% { opacity: .9; r: 36; } }
|
| 165 |
+
.core-halo { animation: breathe 4.5s ease-in-out infinite; }
|
| 166 |
+
.tnode { transition: r .3s var(--ease); }
|
| 167 |
+
|
| 168 |
+
.scrollcue {
|
| 169 |
+
margin-top: clamp(34px, 6vh, 70px); display: flex; align-items: center; gap: 12px;
|
| 170 |
+
font-family: var(--mono); font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase;
|
| 171 |
+
color: var(--text-faint);
|
| 172 |
+
}
|
| 173 |
+
.scrollcue .ln { width: 46px; height: 1px; background: var(--line); position: relative; overflow: hidden; }
|
| 174 |
+
.scrollcue .ln::after { content:""; position:absolute; inset:0; width:40%; background: var(--spark); animation: cue 2.6s var(--ease) infinite; }
|
| 175 |
+
@keyframes cue { 0%{ transform: translateX(-120%);} 60%,100%{ transform: translateX(320%);} }
|
| 176 |
+
|
| 177 |
+
/* ── reveal ── */
|
| 178 |
+
.reveal { opacity: 0; transform: translateY(26px); transition: opacity .9s var(--ease), transform .9s var(--ease); }
|
| 179 |
+
.reveal.in { opacity: 1; transform: none; }
|
| 180 |
+
|
| 181 |
+
/* ── section heading ── */
|
| 182 |
+
.section { padding: clamp(60px, 11vh, 130px) 0; position: relative; }
|
| 183 |
+
.sec-head { max-width: 40em; margin-bottom: 40px; }
|
| 184 |
+
.sec-head h2 {
|
| 185 |
+
font-family: var(--serif); font-weight: 400; font-size: clamp(30px, 4.4vw, 50px);
|
| 186 |
+
line-height: 1.04; letter-spacing: -0.01em; margin: 14px 0 0;
|
| 187 |
+
}
|
| 188 |
+
.sec-head p { color: var(--text-dim); margin: 16px 0 0; font-size: 17px; max-width: 34em; }
|
| 189 |
+
|
| 190 |
+
/* ── ask panel ── */
|
| 191 |
+
.ask-shell {
|
| 192 |
+
display: grid; grid-template-columns: 1.55fr 1fr; gap: 22px;
|
| 193 |
+
}
|
| 194 |
+
.card {
|
| 195 |
+
background: linear-gradient(180deg, oklch(0.205 0.013 264 / 0.7), oklch(0.18 0.012 264 / 0.7));
|
| 196 |
+
border: 1px solid var(--line-soft);
|
| 197 |
+
border-radius: 18px;
|
| 198 |
+
padding: 22px;
|
| 199 |
+
position: relative;
|
| 200 |
+
}
|
| 201 |
+
.seg {
|
| 202 |
+
display: inline-flex; padding: 3px; gap: 2px;
|
| 203 |
+
background: var(--ink-0); border: 1px solid var(--line-soft); border-radius: 99px;
|
| 204 |
+
margin-bottom: 18px;
|
| 205 |
+
}
|
| 206 |
+
.seg button {
|
| 207 |
+
appearance: none; border: 0; background: transparent; cursor: pointer;
|
| 208 |
+
font-family: var(--ui); font-weight: 600; font-size: 13px;
|
| 209 |
+
color: var(--text-faint); padding: 7px 16px; border-radius: 99px;
|
| 210 |
+
transition: color .2s, background .2s;
|
| 211 |
+
}
|
| 212 |
+
.seg button[aria-pressed="true"] { background: var(--ink-2); color: var(--text); }
|
| 213 |
+
|
| 214 |
+
.ask-box {
|
| 215 |
+
width: 100%; min-height: 96px; resize: vertical;
|
| 216 |
+
background: var(--ink-0); border: 1px solid var(--line-soft);
|
| 217 |
+
border-radius: 12px; padding: 15px 16px;
|
| 218 |
+
color: var(--text); font-family: var(--ui); font-size: 16px; line-height: 1.5;
|
| 219 |
+
outline: none; transition: border-color .2s, box-shadow .2s;
|
| 220 |
+
}
|
| 221 |
+
.ask-box:focus { border-color: var(--spark-line); box-shadow: 0 0 0 4px var(--spark-soft); }
|
| 222 |
+
.ask-box::placeholder { color: var(--text-faint); }
|
| 223 |
+
|
| 224 |
+
.ask-row { display: flex; align-items: center; justify-content: flex-end; gap: 14px; margin-top: 14px; flex-wrap: wrap; }
|
| 225 |
+
.run-hint { margin-right: auto; font-family: var(--mono); font-size: 11px; color: var(--text-faint); letter-spacing: 0.04em; display: flex; align-items: center; gap: 6px; }
|
| 226 |
+
.run-hint .kbd { border: 1px solid var(--line-soft); border-bottom-width: 2px; border-radius: 4px; padding: 1px 6px; color: var(--text-dim); background: var(--ink-0); }
|
| 227 |
+
|
| 228 |
+
/* example questions — visible list */
|
| 229 |
+
.examples-block { margin-top: 22px; }
|
| 230 |
+
.examples-label { font-family: var(--mono); font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--text-faint); }
|
| 231 |
+
.examples { list-style: none; margin: 12px 0 0; padding: 0; display: flex; flex-direction: column; gap: 1px; border: 1px solid var(--line-soft); border-radius: 12px; overflow: hidden; }
|
| 232 |
+
.chip {
|
| 233 |
+
appearance: none; cursor: pointer; width: 100%; text-align: left;
|
| 234 |
+
font-family: var(--ui); font-size: 14.5px; color: var(--text-dim);
|
| 235 |
+
background: oklch(0.205 0.013 264 / 0.4); border: 0;
|
| 236 |
+
padding: 13px 15px; display: flex; align-items: center; gap: 12px;
|
| 237 |
+
transition: color .18s, background .18s, padding .18s var(--ease);
|
| 238 |
+
}
|
| 239 |
+
.chip + .chip { border-top: 1px solid var(--line-soft); }
|
| 240 |
+
.chip .ix { font-family: var(--mono); font-size: 11px; color: var(--text-faint); flex-shrink: 0; transition: color .18s; }
|
| 241 |
+
.chip .qt { flex: 1; }
|
| 242 |
+
.chip .go { font-family: var(--mono); color: var(--spark); opacity: 0; transform: translateX(-6px); transition: opacity .18s, transform .18s var(--ease); flex-shrink: 0; }
|
| 243 |
+
.chip:hover { color: var(--text); background: oklch(0.235 0.014 264 / 0.7); padding-left: 19px; }
|
| 244 |
+
.chip:hover .ix { color: var(--spark); }
|
| 245 |
+
.chip:hover .go { opacity: 1; transform: none; }
|
| 246 |
+
|
| 247 |
+
.ask-btn {
|
| 248 |
+
appearance: none; border: 0; cursor: pointer; flex-shrink: 0;
|
| 249 |
+
font-family: var(--ui); font-weight: 700; font-size: 15px;
|
| 250 |
+
color: oklch(0.2 0.04 70);
|
| 251 |
+
background: linear-gradient(180deg, var(--spark), var(--spark-deep));
|
| 252 |
+
padding: 12px 24px; border-radius: 12px;
|
| 253 |
+
display: inline-flex; align-items: center; gap: 12px;
|
| 254 |
+
box-shadow: 0 1px 0 oklch(1 0 0 / 0.3) inset, 0 10px 26px -12px var(--spark-deep);
|
| 255 |
+
transition: transform .2s var(--ease), filter .2s;
|
| 256 |
+
}
|
| 257 |
+
.ask-btn:hover { transform: translateY(-1px); filter: brightness(1.04); }
|
| 258 |
+
.ask-btn small { font-family: var(--mono); font-weight: 400; font-size: 10.5px; opacity: 0.7; letter-spacing: 0.04em; }
|
| 259 |
+
.ask-btn.busy { animation: pulseBtn 1.1s ease-in-out infinite; }
|
| 260 |
+
@keyframes pulseBtn { 0%,100%{ filter: brightness(1);} 50%{ filter: brightness(1.18);} }
|
| 261 |
+
|
| 262 |
+
/* answers */
|
| 263 |
+
.answers { margin-top: 22px; display: grid; gap: 16px; }
|
| 264 |
+
.answers.compare { grid-template-columns: 1fr 1fr; }
|
| 265 |
+
.ans {
|
| 266 |
+
border: 1px solid var(--line-soft); border-radius: 12px; padding: 16px;
|
| 267 |
+
background: var(--ink-0);
|
| 268 |
+
}
|
| 269 |
+
.ans .tag {
|
| 270 |
+
font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.14em; text-transform: uppercase;
|
| 271 |
+
display: flex; align-items: center; gap: 8px; margin-bottom: 12px;
|
| 272 |
+
}
|
| 273 |
+
.ans.before .tag { color: var(--text-faint); }
|
| 274 |
+
.ans.after .tag { color: var(--spark); }
|
| 275 |
+
.ans .tag .d { width: 6px; height: 6px; border-radius: 50%; background: currentColor; box-shadow: 0 0 8px currentColor; }
|
| 276 |
+
.ans .body { font-size: 14.5px; line-height: 1.6; color: var(--text-dim); white-space: pre-wrap; min-height: 3em; }
|
| 277 |
+
.ans.after .body { color: var(--text); }
|
| 278 |
+
.ans.before { opacity: 0.72; }
|
| 279 |
+
.ans .body .caret { color: var(--spark); animation: blink 1s step-end infinite; }
|
| 280 |
+
@keyframes blink { 50% { opacity: 0; } }
|
| 281 |
+
.placeholder-note { color: var(--text-faint); font-size: 13.5px; font-style: italic; }
|
| 282 |
+
@media (max-width: 640px){ .answers.compare { grid-template-columns: 1fr; } }
|
| 283 |
+
|
| 284 |
+
/* teacher influence */
|
| 285 |
+
.influence .head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 4px; }
|
| 286 |
+
.influence .head .ttl { font-family: var(--mono); font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--text-faint); }
|
| 287 |
+
.influence > p { color: var(--text-dim); font-size: 13.5px; margin: 0 0 20px; }
|
| 288 |
+
.inf-rows { display: flex; flex-direction: column; gap: 15px; }
|
| 289 |
+
.inf-row { display: grid; grid-template-columns: 78px 1fr 34px; align-items: center; gap: 12px; }
|
| 290 |
+
.inf-row .nm { font-family: var(--mono); font-size: 12.5px; }
|
| 291 |
+
.inf-row .track { height: 5px; border-radius: 99px; background: oklch(0.3 0.01 264 / 0.5); overflow: hidden; position: relative; }
|
| 292 |
+
.inf-row .fill { position: absolute; inset: 0 auto 0 0; width: 0; border-radius: 99px; background: currentColor; box-shadow: 0 0 10px currentColor; transition: width 1s var(--ease); }
|
| 293 |
+
.inf-row .pc { font-family: var(--mono); font-size: 11.5px; color: var(--text-faint); text-align: right; }
|
| 294 |
+
.inf-foot { margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--line-soft);
|
| 295 |
+
font-family: var(--mono); font-size: 11px; color: var(--text-faint); display: flex; align-items: center; gap: 8px; }
|
| 296 |
+
.inf-foot .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--spark); box-shadow: 0 0 8px var(--spark); }
|
| 297 |
+
|
| 298 |
+
/* ── thought map ── */
|
| 299 |
+
.map-stage {
|
| 300 |
+
position: relative; height: clamp(440px, 64vh, 680px);
|
| 301 |
+
border-radius: 22px; overflow: hidden;
|
| 302 |
+
border: 1px solid var(--line-soft);
|
| 303 |
+
background:
|
| 304 |
+
radial-gradient(60% 60% at 50% 50%, oklch(0.2 0.013 264 / 0.5), transparent 75%),
|
| 305 |
+
var(--ink-0);
|
| 306 |
+
}
|
| 307 |
+
.map-stage .vignette { position: absolute; inset: 0; pointer-events: none;
|
| 308 |
+
box-shadow: inset 0 0 140px 30px oklch(0.13 0.01 264 / 0.9); }
|
| 309 |
+
.map-layer { position: absolute; inset: 0; will-change: transform; }
|
| 310 |
+
.dot {
|
| 311 |
+
position: absolute; border-radius: 50%; transform: translate(-50%, -50%);
|
| 312 |
+
box-shadow: 0 0 10px currentColor; will-change: transform;
|
| 313 |
+
}
|
| 314 |
+
.dot.probe { background: var(--text); color: var(--text); }
|
| 315 |
+
.dot.probe::after { content:""; position:absolute; inset:-7px; border-radius:50%; border:1px solid var(--text); opacity:0; animation: ping 1.6s var(--ease) 2; }
|
| 316 |
+
@keyframes ping { 0%{ transform: scale(.4); opacity:.8;} 100%{ transform: scale(1.6); opacity:0; } }
|
| 317 |
+
.map-legend {
|
| 318 |
+
position: absolute; left: 20px; bottom: 18px; z-index: 3;
|
| 319 |
+
display: flex; flex-wrap: wrap; gap: 14px;
|
| 320 |
+
}
|
| 321 |
+
.map-legend .lg { display: flex; align-items: center; gap: 7px; font-family: var(--mono); font-size: 11px; color: var(--text-dim); }
|
| 322 |
+
.map-legend .lg .s { width: 8px; height: 8px; border-radius: 50%; box-shadow: 0 0 7px currentColor; background: currentColor; }
|
| 323 |
+
.map-cap { position: absolute; top: 20px; left: 22px; z-index: 3; max-width: 30em; }
|
| 324 |
+
.map-cap .t { font-family: var(--mono); font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--text-faint); }
|
| 325 |
+
.map-cap p { margin: 8px 0 0; color: var(--text-dim); font-size: 14px; }
|
| 326 |
+
|
| 327 |
+
/* ── footer ── */
|
| 328 |
+
footer { position: relative; z-index: 2; border-top: 1px solid var(--line-soft); margin-top: 40px; }
|
| 329 |
+
.foot-in { max-width: var(--maxw); margin: 0 auto; padding: 30px clamp(20px,4vw,40px);
|
| 330 |
+
display: flex; align-items: center; justify-content: space-between; gap: 20px; flex-wrap: wrap; }
|
| 331 |
+
.foot-in .tag { font-family: var(--serif); font-size: 22px; color: var(--text-dim); }
|
| 332 |
+
.foot-in .tag .em { font-style: italic; color: var(--spark); }
|
| 333 |
+
.foot-links { display: flex; gap: 22px; font-family: var(--mono); font-size: 12px; }
|
| 334 |
+
.foot-links a { color: var(--text-faint); text-decoration: none; transition: color .2s; }
|
| 335 |
+
.foot-links a:hover { color: var(--spark); }
|
| 336 |
+
|
| 337 |
+
@media (max-width: 900px){
|
| 338 |
+
.hero-grid { grid-template-columns: 1fr; }
|
| 339 |
+
.converge { max-width: 460px; margin: 8px auto 0; }
|
| 340 |
+
.ask-shell { grid-template-columns: 1fr; }
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
@media (prefers-reduced-motion: reduce){
|
| 344 |
+
*, *::before, *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; }
|
| 345 |
+
html { scroll-behavior: auto; }
|
| 346 |
+
.reveal { opacity: 1; transform: none; }
|
| 347 |
+
}
|
| 348 |
+
</style>
|
| 349 |
+
</head>
|
| 350 |
+
<body>
|
| 351 |
+
<div class="ambient" id="ambient"></div>
|
| 352 |
+
<div class="grain"></div>
|
| 353 |
+
|
| 354 |
+
<!-- ── TOP BAR ── -->
|
| 355 |
+
<header class="topbar">
|
| 356 |
+
<div class="brand">
|
| 357 |
+
<span class="name">One for <b>All</b></span>
|
| 358 |
+
</div>
|
| 359 |
+
<div class="stat-row">
|
| 360 |
+
<span class="stat"><span class="d s"></span>Student <b>· 0.5B</b></span>
|
| 361 |
+
<span class="stat" id="statTeachers"><span class="d t"></span>6 Teachers <b>· 1.5–4B</b></span>
|
| 362 |
+
</div>
|
| 363 |
+
</header>
|
| 364 |
+
|
| 365 |
+
<!-- ── HERO ── -->
|
| 366 |
+
<section class="hero">
|
| 367 |
+
<div class="wrap hero-grid">
|
| 368 |
+
<div class="hero-copy">
|
| 369 |
+
<span class="eyebrow">Multi-teacher distillation</span>
|
| 370 |
+
<h1>Six minds,<br />one <span class="em">student</span>.</h1>
|
| 371 |
+
<p class="lede">
|
| 372 |
+
Six specialist AIs pour what they know into a single half-billion-parameter
|
| 373 |
+
model — <b>the strength of many, carried by one.</b> Ask it anything and watch
|
| 374 |
+
the inheritance happen, live.
|
| 375 |
+
</p>
|
| 376 |
+
<div class="hero-cta">
|
| 377 |
+
<button class="btn btn-spark" data-scroll="#try">Ask the student <span class="arrow">↓</span></button>
|
| 378 |
+
<button class="btn btn-ghost" data-scroll="#map">See the thought map</button>
|
| 379 |
+
</div>
|
| 380 |
+
<div class="scrollcue"><span class="ln"></span> watch the transfer</div>
|
| 381 |
+
</div>
|
| 382 |
+
|
| 383 |
+
<!-- convergence diagram: 6 teachers → 1 student -->
|
| 384 |
+
<div class="converge" id="converge" aria-hidden="true">
|
| 385 |
+
<svg viewBox="0 0 600 600">
|
| 386 |
+
<defs>
|
| 387 |
+
<radialGradient id="coreG" cx="50%" cy="50%" r="50%">
|
| 388 |
+
<stop offset="0%" stop-color="oklch(0.97 0.05 80)" />
|
| 389 |
+
<stop offset="45%" stop-color="var(--spark)" />
|
| 390 |
+
<stop offset="100%" stop-color="var(--spark-deep)" />
|
| 391 |
+
</radialGradient>
|
| 392 |
+
<filter id="soft" x="-60%" y="-60%" width="220%" height="220%">
|
| 393 |
+
<feGaussianBlur stdDeviation="7" />
|
| 394 |
+
</filter>
|
| 395 |
+
</defs>
|
| 396 |
+
|
| 397 |
+
<!-- threads + flow + nodes are injected by JS -->
|
| 398 |
+
<g id="threads"></g>
|
| 399 |
+
<g id="flows"></g>
|
| 400 |
+
|
| 401 |
+
<!-- student core -->
|
| 402 |
+
<circle class="core-halo" cx="300" cy="300" r="32" fill="var(--spark)" opacity="0.5" filter="url(#soft)" />
|
| 403 |
+
<g class="core-ring">
|
| 404 |
+
<circle cx="300" cy="300" r="58" fill="none" stroke="var(--spark-line)" stroke-width="1" stroke-dasharray="3 9" />
|
| 405 |
+
</g>
|
| 406 |
+
<circle cx="300" cy="300" r="17" fill="url(#coreG)" />
|
| 407 |
+
<circle cx="300" cy="300" r="17" fill="none" stroke="oklch(1 0 0 / 0.5)" stroke-width="1" />
|
| 408 |
+
<text x="300" y="345" text-anchor="middle" class="tlabel" style="fill: var(--spark); font-size: 12px; letter-spacing:0.12em;">STUDENT · 0.5B</text>
|
| 409 |
+
|
| 410 |
+
<g id="tnodes"></g>
|
| 411 |
+
</svg>
|
| 412 |
+
</div>
|
| 413 |
+
</div>
|
| 414 |
+
</section>
|
| 415 |
+
|
| 416 |
+
<!-- ── ASK / COMPARE ── -->
|
| 417 |
+
<section class="section" id="try">
|
| 418 |
+
<div class="wrap">
|
| 419 |
+
<div class="sec-head reveal">
|
| 420 |
+
<span class="eyebrow">Live probe</span>
|
| 421 |
+
<h2>One prompt. Two answers.<br />Before the teachers, and after.</h2>
|
| 422 |
+
<p>The exact same 0.5-billion-parameter model — with and without what it learned from the
|
| 423 |
+
six teachers. Type a question, or pick one below.</p>
|
| 424 |
+
</div>
|
| 425 |
+
|
| 426 |
+
<div class="ask-shell reveal">
|
| 427 |
+
<!-- left: prompt + answers -->
|
| 428 |
+
<div class="card">
|
| 429 |
+
<div class="seg" role="group" aria-label="view mode">
|
| 430 |
+
<button data-mode="ask" aria-pressed="true">Ask</button>
|
| 431 |
+
<button data-mode="compare" aria-pressed="false">Compare before / after</button>
|
| 432 |
+
</div>
|
| 433 |
+
|
| 434 |
+
<textarea class="ask-box" id="ask" placeholder="Ask anything — a math problem, a science question, a bit of code…">Write a Python function that checks if a word is a palindrome.</textarea>
|
| 435 |
+
|
| 436 |
+
<div class="ask-row">
|
| 437 |
+
<span class="run-hint"><span class="kbd">⌘</span><span class="kbd">↵</span> to run</span>
|
| 438 |
+
<button class="ask-btn" id="askBtn">Ask <small>0.5B model</small></button>
|
| 439 |
+
</div>
|
| 440 |
+
|
| 441 |
+
<div class="examples-block">
|
| 442 |
+
<span class="examples-label">Or pick a question</span>
|
| 443 |
+
<ul class="examples" id="examples"></ul>
|
| 444 |
+
</div>
|
| 445 |
+
|
| 446 |
+
<div class="answers" id="answers">
|
| 447 |
+
<div class="ans after">
|
| 448 |
+
<div class="tag"><span class="d"></span>After · taught by 6 teachers</div>
|
| 449 |
+
<div class="body" id="bodyAfter"><span class="placeholder-note">Ask the student to see how its answer changes after distillation.</span></div>
|
| 450 |
+
</div>
|
| 451 |
+
<div class="ans before" id="beforeCard" hidden>
|
| 452 |
+
<div class="tag"><span class="d"></span>Before · plain base model</div>
|
| 453 |
+
<div class="body" id="bodyBefore"></div>
|
| 454 |
+
</div>
|
| 455 |
+
</div>
|
| 456 |
+
</div>
|
| 457 |
+
|
| 458 |
+
<!-- right: teacher influence -->
|
| 459 |
+
<div class="card influence">
|
| 460 |
+
<div class="head"><span class="ttl">Teacher influence</span></div>
|
| 461 |
+
<p>While the student answers, these bars show how much it leans on what each teacher taught it.</p>
|
| 462 |
+
<div class="inf-rows" id="infRows"></div>
|
| 463 |
+
<div class="inf-foot"><span class="dot"></span><span id="infNote">idle — waiting for a prompt</span></div>
|
| 464 |
+
</div>
|
| 465 |
+
</div>
|
| 466 |
+
</div>
|
| 467 |
+
</section>
|
| 468 |
+
|
| 469 |
+
<!-- ── THOUGHT MAP ── -->
|
| 470 |
+
<section class="section" id="map">
|
| 471 |
+
<div class="wrap">
|
| 472 |
+
<div class="sec-head reveal">
|
| 473 |
+
<span class="eyebrow">Representation space</span>
|
| 474 |
+
<h2>Where every mind keeps its thoughts.</h2>
|
| 475 |
+
<p>Each dot is how one model internally represents a piece of text. Teachers cluster by what
|
| 476 |
+
they're good at — and the student drifts toward all of them at once. Your prompts land here
|
| 477 |
+
as bright probes.</p>
|
| 478 |
+
</div>
|
| 479 |
+
|
| 480 |
+
<div class="map-stage reveal" id="mapStage">
|
| 481 |
+
<div class="map-cap">
|
| 482 |
+
<div class="t">Thought map</div>
|
| 483 |
+
<p>Move your cursor — the field has depth.</p>
|
| 484 |
+
</div>
|
| 485 |
+
<div class="map-layer" id="mapFar" data-depth="0.012"></div>
|
| 486 |
+
<div class="map-layer" id="mapMid" data-depth="0.028"></div>
|
| 487 |
+
<div class="map-layer" id="mapNear" data-depth="0.05"></div>
|
| 488 |
+
<div class="vignette"></div>
|
| 489 |
+
<div class="map-legend" id="mapLegend"></div>
|
| 490 |
+
</div>
|
| 491 |
+
</div>
|
| 492 |
+
</section>
|
| 493 |
+
|
| 494 |
+
<footer>
|
| 495 |
+
<div class="foot-in">
|
| 496 |
+
<div class="tag">one for all — <span class="em">the strength of many, carried by one.</span></div>
|
| 497 |
+
<div class="foot-links">
|
| 498 |
+
<a href="https://huggingface.co/build-small-hackathon/deku" target="_blank" rel="noopener">model</a>
|
| 499 |
+
<a href="https://huggingface.co/build-small-hackathon/deku-gguf" target="_blank" rel="noopener">gguf</a>
|
| 500 |
+
<a href="https://huggingface.co/spaces/build-small-hackathon/one-for-all" target="_blank" rel="noopener">↗ space</a>
|
| 501 |
+
</div>
|
| 502 |
+
</div>
|
| 503 |
+
</footer>
|
| 504 |
+
|
| 505 |
+
<script type="module" src="/static/app.js"></script>
|
| 506 |
+
</body>
|
| 507 |
+
</html>
|
requirements.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
gradio=
|
| 2 |
plotly>=5.20
|
| 3 |
umap-learn>=0.5
|
| 4 |
numpy
|
|
@@ -9,3 +9,7 @@ accelerate>=0.30
|
|
| 9 |
spaces
|
| 10 |
huggingface_hub>=0.22
|
| 11 |
trimesh>=4.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=6.18
|
| 2 |
plotly>=5.20
|
| 3 |
umap-learn>=0.5
|
| 4 |
numpy
|
|
|
|
| 9 |
spaces
|
| 10 |
huggingface_hub>=0.22
|
| 11 |
trimesh>=4.0
|
| 12 |
+
# llama.cpp backend (OFA_BACKEND=llamacpp) is opt-in: it compiles from source on
|
| 13 |
+
# the Spaces builder (~10 min, can fail), and the default torch/ZeroGPU path never
|
| 14 |
+
# imports it. Uncomment only if you switch OFA_BACKEND to "llamacpp".
|
| 15 |
+
# llama-cpp-python>=0.3
|
server_app.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
space/server_app.py — One for All, custom frontend on gr.Server.
|
| 3 |
+
|
| 4 |
+
gr.Server = FastAPI with Gradio's API engine: @app.api functions become
|
| 5 |
+
queued/streaming endpoints (SSE), and we serve our own HTML/JS at "/" —
|
| 6 |
+
the browser talks to the endpoints via @gradio/client (required for ZeroGPU).
|
| 7 |
+
|
| 8 |
+
Run locally:
|
| 9 |
+
cd space && VIZ_DATA_PATH=/path/to/viz_data.json python server_app.py
|
| 10 |
+
|
| 11 |
+
The legacy Blocks UI (app.py) keeps working — both share _boot.py.
|
| 12 |
+
"""
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
|
| 17 |
+
import spaces
|
| 18 |
+
from fastapi.responses import FileResponse, HTMLResponse
|
| 19 |
+
from gradio import Server
|
| 20 |
+
|
| 21 |
+
import _boot
|
| 22 |
+
|
| 23 |
+
RT = _boot.load_runtime()
|
| 24 |
+
FRONTEND = Path(__file__).parent / "frontend"
|
| 25 |
+
|
| 26 |
+
app = Server()
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
# ── API: static viz payload (soul space, CKA, curves, meta) ────────────────
|
| 30 |
+
@app.api(name="viz")
|
| 31 |
+
def viz() -> dict:
|
| 32 |
+
return _boot.viz_payload(RT.viz, RT.coords3d, RT.backend, RT.model_ready)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
# ── API: streaming probe — text + live gates, final soul-space point ───────
|
| 36 |
+
@app.api(name="probe", stream_every=0.1)
|
| 37 |
+
@spaces.GPU
|
| 38 |
+
def probe(text: str) -> dict: # annotation = type of each streamed chunk
|
| 39 |
+
if not text.strip() or not RT.model_ready:
|
| 40 |
+
yield {"error": "model not ready" if text.strip() else "empty prompt",
|
| 41 |
+
"done": True}
|
| 42 |
+
return
|
| 43 |
+
_boot.to_device(RT)
|
| 44 |
+
partial, gates = "", []
|
| 45 |
+
for partial, gates in _boot.stream(RT, text):
|
| 46 |
+
yield {"text": partial, "gates": gates, "done": False}
|
| 47 |
+
point = None
|
| 48 |
+
if RT.reducer is not None:
|
| 49 |
+
point, gates = _boot.final_probe(RT, text)
|
| 50 |
+
yield {"text": partial, "gates": gates, "point": point, "done": True}
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
# ── API: streaming arena — base (LoRA off) vs deku, interleaved ────────────
|
| 54 |
+
@app.api(name="arena", stream_every=0.1)
|
| 55 |
+
@spaces.GPU
|
| 56 |
+
def arena(text: str) -> dict: # annotation = type of each streamed chunk
|
| 57 |
+
if not text.strip() or not RT.model_ready:
|
| 58 |
+
yield {"error": "model not ready" if text.strip() else "empty prompt",
|
| 59 |
+
"done": True}
|
| 60 |
+
return
|
| 61 |
+
_boot.to_device(RT)
|
| 62 |
+
base = deku = ""
|
| 63 |
+
gates: list[float] = []
|
| 64 |
+
for base, deku, gates in _boot.stream_pair(RT, text):
|
| 65 |
+
yield {"base": base, "deku": deku, "gates": gates, "done": False}
|
| 66 |
+
yield {"base": base, "deku": deku, "gates": gates, "done": True}
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
# ── Custom frontend (overrides Gradio's default UI at "/") ────────────────
|
| 70 |
+
@app.get("/", response_class=HTMLResponse)
|
| 71 |
+
async def home():
|
| 72 |
+
return (FRONTEND / "index.html").read_text()
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
@app.get("/static/{name}")
|
| 76 |
+
async def static(name: str):
|
| 77 |
+
target = (FRONTEND / name).resolve()
|
| 78 |
+
if target.parent != FRONTEND.resolve() or not target.exists():
|
| 79 |
+
return HTMLResponse("not found", status_code=404)
|
| 80 |
+
return FileResponse(target)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
if __name__ == "__main__":
|
| 84 |
+
app.launch()
|