Quazim0t0 commited on
Commit
0568a46
Β·
verified Β·
1 Parent(s): 947ff9b

Default generation = base-pair (FNS), marginalized per-base sampling

Browse files
Files changed (1) hide show
  1. app.py +480 -480
app.py CHANGED
@@ -1,480 +1,480 @@
1
- """
2
- DaisyChain β€” interactive routing demo (HuggingFace Space).
3
-
4
- Paste DNA; the learned router reads how *surprised* each ~74M specialist is (bits/base)
5
- plus its hidden state and hands the sequence to its home specialist β€” then that specialist
6
- streams a continuation live. Styled after the Modular-Mind panel: animated routing cards,
7
- a first-run loading notice, live token streaming. Every handler is a generator.
8
- """
9
- import html as _h
10
- import os
11
- import json
12
- import math
13
-
14
- import gradio as gr
15
-
16
- # ZeroGPU: @spaces.GPU allocates a GPU only for the decorated call. Falls back to a no-op
17
- # decorator when `spaces` isn't installed (local / plain CPU).
18
- try:
19
- import spaces
20
- _gpu = spaces.GPU
21
- except Exception:
22
- def _gpu(fn=None, **kw):
23
- return fn if callable(fn) else (lambda f: f)
24
-
25
- from daisychain import DaisyChain
26
-
27
- HERE = os.path.dirname(os.path.abspath(__file__))
28
- MODEL_REPO = os.environ.get("DAISYCHAIN_REPO", "DaisyChainAI/daisychain-genomics")
29
- DEVICE = os.environ.get("DAISYCHAIN_DEVICE", "cpu")
30
-
31
- # code + tokenizer + router are bundled here; pull the big specialist weights from the
32
- # (gated) model repo on first launch using the HF_TOKEN Space secret. No silent
33
- # swallow β€” if the download fails we want a visible error, not a broken-but-running app.
34
- if not os.path.exists(os.path.join(HERE, "eukaryote", "model.safetensors")):
35
- from huggingface_hub import snapshot_download
36
- snapshot_download(MODEL_REPO, local_dir=HERE,
37
- token=os.environ.get("HF_TOKEN"),
38
- allow_patterns=["*/model.safetensors", "tokenizer.json", "router2.pt"])
39
-
40
- _DC = {"m": None} # lazy-loaded so CUDA is never touched at import
41
- _WARMED = {"done": False} # so the "loading" notice only shows on the first run
42
-
43
- EMOJI = {"eukaryote": "🧬 Eukaryote", "prokaryote": "🦠 Prokaryote",
44
- "mrna": "πŸ“œ mRNA", "mrna_splice": "βœ‚οΈ mRNA-splice"}
45
- # deeper, paper-friendly tones (vs the old neon dark-theme hues)
46
- COLOR = {"eukaryote": "#5b4bb0", "prokaryote": "#1f7a99",
47
- "mrna": "#b03a63", "mrna_splice": "#317f3f"}
48
- DESC = DaisyChain.DESCRIPTIONS
49
-
50
-
51
- def _moe():
52
- if _DC["m"] is None:
53
- _DC["m"] = DaisyChain(root=HERE, device=DEVICE)
54
- return _DC["m"]
55
-
56
-
57
- # ---- HTML rendering ----------------------------------------------------------------
58
- # Editorial "scientific-paper" aesthetic borrowed from Carbon's demo (cream paper, ink,
59
- # green accent, mono uppercase headers) but made our own: a daisy-gold second accent and
60
- # the chain-of-specialists motif. Tokens live in :root so the cards/bars all stay in sync.
61
- _CSS = """<style>
62
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&family=JetBrains+Mono:wght@400;500;700&display=swap');
63
- :root{--paper:#f7f5ee;--paper2:#f2efe2;--ink:#1f1f1d;--muted:#8c918b;--hairline:#d6d3c4;
64
- --green:#317f3f;--green2:#2a5931;--gold:#c79a2e;--gold2:#9c7714}
65
- .dcx{font-family:"Inter","Helvetica Neue",sans-serif;font-weight:300;color:var(--ink);
66
- font-size:13px;line-height:1.7;margin:4px 0}
67
- .dcx .mono{font-family:"JetBrains Mono",ui-monospace,monospace}
68
- .dcx .note{background:var(--paper2);border:1px solid var(--hairline);border-radius:4px;
69
- padding:12px 14px;color:#5a5a55;font-size:13px}
70
- .dcx .h{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:12px;font-weight:500;
71
- letter-spacing:.16em;text-transform:uppercase;color:var(--ink);margin:6px 0 12px;
72
- padding-bottom:8px;border-bottom:1px solid var(--hairline)}
73
- .dcx .p{color:var(--muted)}
74
- .dcx .g{color:var(--green2);font-weight:600}
75
- .dcx .chain{display:flex;gap:0;align-items:stretch;flex-wrap:wrap;margin:8px 0}
76
- .dcx .link{align-self:center;color:var(--hairline);font-size:18px;margin:0 2px 22px;font-weight:700}
77
- .dcx .card{flex:1;min-width:180px;background:var(--paper);border:1px solid var(--hairline);
78
- border-radius:4px;padding:12px 13px;position:relative;overflow:hidden;transition:box-shadow .3s,border-color .3s}
79
- .dcx .card .nm{font-family:"JetBrains Mono",ui-monospace,monospace;font-weight:500;font-size:12px;
80
- letter-spacing:.06em;text-transform:uppercase}
81
- .dcx .card .meta{color:var(--muted);font-size:11px;margin-top:4px;min-height:28px;line-height:1.45}
82
- .dcx .card .bar{height:6px;background:var(--paper2);border:1px solid var(--hairline);border-radius:99px;margin-top:9px;overflow:hidden}
83
- .dcx .card .fill{height:100%;border-radius:99px;animation:dcxw .7s ease}
84
- .dcx .card .pct{font-size:10px;color:var(--muted);margin-top:5px;letter-spacing:.04em;text-transform:uppercase;font-family:"JetBrains Mono",monospace}
85
- .dcx .badge{position:absolute;top:9px;right:10px;font-family:"JetBrains Mono",monospace;font-size:9px;
86
- font-weight:700;letter-spacing:.12em;padding:3px 8px;border-radius:99px;color:var(--paper);background:var(--green)}
87
- @keyframes dcxw{from{width:0}}
88
- .dcx .gen{background:var(--paper2);border:1px solid var(--hairline);border-radius:4px;padding:13px 15px;
89
- margin:12px 0;font-size:14px;line-height:1.8;font-family:"JetBrains Mono",ui-monospace,monospace;word-break:break-all}
90
- .dcx .caret{display:inline-block;width:8px;height:16px;border-radius:1px;background:var(--green);
91
- margin-left:2px;vertical-align:text-bottom;animation:dcxb .8s steps(1) infinite}
92
- @keyframes dcxb{50%{opacity:0}}
93
- .dcx .sub{color:var(--muted);font-size:12px;line-height:1.6;margin-top:10px}
94
- .dcx .stats{display:flex;gap:0;flex-wrap:wrap;margin:12px 0;border:1px solid var(--hairline);border-radius:4px;overflow:hidden}
95
- .dcx .stat{flex:1;min-width:120px;text-align:center;background:var(--paper);padding:16px 10px;border-right:1px solid var(--hairline)}
96
- .dcx .stat:last-child{border-right:none}
97
- .dcx .stat .v{font-family:"JetBrains Mono",monospace;font-size:26px;font-weight:700;line-height:1;color:var(--green2)}
98
- .dcx .stat .l{font-size:10px;color:var(--muted);margin-top:8px;letter-spacing:.06em;text-transform:uppercase;line-height:1.4}
99
- .dcx table{border-collapse:collapse;width:100%;margin:10px 0;font-size:12.5px}
100
- .dcx th,.dcx td{border:1px solid var(--hairline);padding:8px 11px;text-align:left}
101
- .dcx th{background:var(--paper2);color:var(--ink);font-family:"JetBrains Mono",monospace;font-weight:500;
102
- font-size:10px;letter-spacing:.1em;text-transform:uppercase}
103
- .dcx td.n{text-align:right;font-variant-numeric:tabular-nums;font-family:"JetBrains Mono",monospace}
104
- /* --- live 2D double-helix (our own take on a base-by-base DNA viz) --- */
105
- .dcx .helixwrap{background:var(--paper2);border:1px solid var(--hairline);border-radius:4px;padding:8px 12px;margin:12px 0;overflow-x:auto}
106
- .dcx .helix-svg{display:block;margin:1px 0}
107
- .dcx .hx-strand{fill:none;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;opacity:.5}
108
- .dcx .hx-strand.back{opacity:.24}
109
- .dcx .hx-rung{stroke:var(--hairline);stroke-width:1}
110
- .dcx .hx-rung.user{stroke:#e6e3d6}
111
- .dcx .hx-base{font-family:"JetBrains Mono",monospace;font-size:9px;font-weight:700;text-anchor:middle;dominant-baseline:central}
112
- .dcx .hx-legend{display:flex;gap:13px;flex-wrap:wrap;font-family:"JetBrains Mono",monospace;font-size:9px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);margin:2px 0 4px}
113
- .dcx .hx-dot{display:inline-block;width:8px;height:8px;border-radius:2px;margin-right:5px;vertical-align:middle}
114
- /* --- tokenization tracks (our take on carbon-tokenization-02): per-base + 6-mer --- */
115
- .dcx .seqtrack{font-family:"JetBrains Mono",monospace;font-size:11px;background:var(--paper);border:1px solid var(--hairline);border-radius:4px;padding:8px 10px;display:flex;flex-wrap:wrap;gap:1px;margin:8px 0 2px}
116
- .dcx .seqb{display:inline-flex;align-items:center;justify-content:center;width:18px;height:20px;border-radius:2px;color:#fff;font-weight:500}
117
- .dcx .seqb.A{background:#1A7A40}.dcx .seqb.T{background:#b00020}.dcx .seqb.C{background:#2c5aa0}.dcx .seqb.G{background:#b8862c}
118
- .dcx .tkrow{display:flex;gap:14px;align-items:baseline;margin:12px 0 2px;flex-wrap:wrap}
119
- .dcx .tklabel{font-family:"JetBrains Mono",monospace;font-size:9.5px;font-weight:500;color:#5b5b56;text-transform:uppercase;letter-spacing:1.6px}
120
- .dcx .tkstat{font-family:"JetBrains Mono",monospace;font-size:10px;color:var(--muted);letter-spacing:.5px}
121
- .dcx .tkstat .n{font-weight:600;color:var(--green2)}
122
- .dcx .tokrow{display:flex;flex-wrap:wrap;align-items:center;gap:0}
123
- .dcx .tok{display:inline-flex;align-items:center;font-family:"JetBrains Mono",monospace;font-size:11px;letter-spacing:.5px;padding:4px 8px;margin:2px;border:1px solid #ccc;border-radius:3px;background:#fff;color:var(--ink)}
124
- .dcx .tok.kmer{background:rgba(49,127,63,.10);border-color:rgba(49,127,63,.5);color:var(--green2);font-weight:500}
125
- /* --- editorial hero banner (dotted texture + faint green edge stripes) --- */
126
- .dc-banner{position:relative;overflow:hidden;border:1px solid var(--hairline);border-radius:6px;margin:2px 0 6px;
127
- background:radial-gradient(circle at 22% 32%,rgba(0,0,0,.06),transparent 1px),
128
- radial-gradient(circle at 78% 64%,rgba(0,0,0,.055),transparent 1px),
129
- linear-gradient(90deg,rgba(49,127,63,.04),transparent 32%,transparent 68%,rgba(199,154,46,.05)),
130
- var(--paper);
131
- background-size:7px 7px,11px 11px,auto,auto}
132
- .dc-binner{position:relative;padding:22px 26px;font-family:"JetBrains Mono",ui-monospace,monospace}
133
- .dc-ident{display:flex;align-items:center;gap:11px;margin-bottom:16px}
134
- .dc-mark{font-size:30px;line-height:1}
135
- .dc-title{font-size:13px;font-weight:700;letter-spacing:.2em;text-transform:uppercase;color:var(--ink)}
136
- .dc-path{font-size:10px;font-weight:400;letter-spacing:.18em;text-transform:uppercase;color:var(--muted);margin-top:2px}
137
- .dc-word{font-family:"JetBrains Mono",monospace;font-size:40px;font-weight:700;letter-spacing:-.01em;color:var(--ink);line-height:1.05;margin:4px 0 6px}
138
- .dc-word .dot{color:var(--gold)}
139
- .dc-tag{font-family:"Inter",sans-serif;font-weight:300;font-size:13px;color:#5a5a55;max-width:560px;line-height:1.6}
140
- .dc-motif{display:flex;align-items:center;gap:4px;flex-wrap:wrap;margin-top:18px}
141
- .dc-chip{font-family:"JetBrains Mono",monospace;font-size:10px;letter-spacing:.06em;text-transform:uppercase;
142
- background:var(--paper2);border:1px solid var(--hairline);border-radius:99px;padding:5px 11px;color:var(--ink)}
143
- .dc-tie{color:var(--green);font-size:13px;font-weight:700}
144
- .dc-rule{height:1px;background:var(--hairline);margin:14px 0}
145
- </style>"""
146
-
147
-
148
- def _wrap(body):
149
- return _CSS + "<div class='dcx'>" + body + "</div>"
150
-
151
-
152
- def _esc(s):
153
- return _h.escape(s or "").replace("\n", "<br>")
154
-
155
-
156
- def _notice(action="Routing"):
157
- if not _WARMED["done"]:
158
- try:
159
- gr.Info("First run β€” loading the four ~74M specialists (~20–40s on CPU). After this it's quick.")
160
- except Exception:
161
- pass
162
- return _wrap(f"<div class='note'>⏳ Loading the four ~74M specialists + {action.lower()}… "
163
- "first run can take ~20–40s on CPU; every run after is fast.</div>")
164
- return _wrap(f"<div class='note'>⏳ {action}…</div>")
165
-
166
-
167
- def _msg(title, body):
168
- return _wrap(f"<div class='note'><b>{title}</b><br>{body}</div>")
169
-
170
-
171
- def _cards(bpb, winner=None):
172
- """One animated card per specialist: surprise (bits/base), confidence bar, winner badge + glow.
173
- bpb values may be None (not computed yet). Lower bits/base = more 'at home' = fuller bar."""
174
- cells = []
175
- doms = list(bpb.keys())
176
- for i, n in enumerate(doms):
177
- c = COLOR.get(n, "#9b59b6")
178
- v = bpb[n]
179
- win = (n == winner)
180
- conf = max(0.0, min(1.0, (2.02 - v) / 0.5)) if v is not None else 0.0 # ~1.52..2.02 -> 1..0
181
- style = f"border-color:{c};box-shadow:0 0 16px {c}40" if win else ""
182
- badge = f"<span class='badge' style='background:{c}'>ROUTED βœ“</span>" if win else ""
183
- meta = (f"{DESC.get(n,'')}<br>{v:.3f} bits/base (lower = more at home)"
184
- if v is not None else f"{DESC.get(n,'')}<br>…")
185
- bar = (f"<div class='bar'><div class='fill' style='width:{conf*100:.1f}%;background:{c}'></div></div>"
186
- f"<div class='pct'>confidence {conf*100:.0f}%</div>") if v is not None else \
187
- "<div class='bar'></div><div class='pct'>…</div>"
188
- cells.append(
189
- f"<div class='card' style='{style}'>{badge}"
190
- f"<div class='nm' style='color:{c}'>{EMOJI.get(n, n)}</div>"
191
- f"<div class='meta'>{meta}</div>{bar}</div>")
192
- if i < len(doms) - 1:
193
- cells.append("<div class='link'>β¬­</div>")
194
- return "<div class='chain'>" + "".join(cells) + "</div>"
195
-
196
-
197
- def _gen_box(prompt, gen, live=False):
198
- caret = "<span class='caret'></span>" if live else ""
199
- return (f"<div class='gen'><span class='p'>{_esc(prompt)}</span>"
200
- f"<span class='g'>{_esc(gen)}</span>{caret}</div>")
201
-
202
-
203
- # ---- live 2D double-helix --------------------------------------------------------
204
- # A base-by-base SVG double-helix (two sine-wave backbones + per-base rungs, each base
205
- # letter on the top strand with its Watson–Crick complement on the bottom). Built in
206
- # Python so it streams inside our existing generator; ours = cream palette, our own
207
- # nucleotide colors, strands tinted by the routed specialist's accent.
208
- _COMP = {"A": "T", "T": "A", "C": "G", "G": "C", "N": "N"}
209
- # shared nucleotide palette (matches our tokenization track): A green, T red, C blue, G amber
210
- _BASE_COL = {"A": "#1A7A40", "T": "#b00020", "C": "#2c5aa0", "G": "#b8862c"}
211
- _USER_COL = "#bdbaa9"
212
- _HX_SP, _HX_AMP, _HX_YC, _HX_ROWH, _HX_TURN, _HX_PERROW = 14, 12, 22, 48, 10.5, 46
213
-
214
-
215
- def _hx_strand(n, sign):
216
- pts = []
217
- for s in range(n * 4 + 1):
218
- t = s / 4
219
- x = t * _HX_SP + _HX_SP / 2
220
- ang = (t + 0.5) * 2 * math.pi / _HX_TURN
221
- y = _HX_YC + sign * _HX_AMP * math.sin(ang)
222
- pts.append(f"{x:.1f},{y:.1f}")
223
- return " ".join(pts)
224
-
225
-
226
- def _hx_row(bases, start, user_len, accent):
227
- n = len(bases)
228
- w = n * _HX_SP + 6
229
- out = [f"<svg class='helix-svg' width='{w}' height='{_HX_ROWH}' "
230
- f"viewBox='0 0 {w} {_HX_ROWH}' xmlns='http://www.w3.org/2000/svg'>",
231
- f"<polyline class='hx-strand' style='stroke:{accent}' points='{_hx_strand(n,1)}'/>",
232
- f"<polyline class='hx-strand back' style='stroke:{accent}' points='{_hx_strand(n,-1)}'/>"]
233
- for i in range(n):
234
- x = i * _HX_SP + _HX_SP / 2
235
- ang = (i + 0.5) * 2 * math.pi / _HX_TURN
236
- yt = _HX_YC + _HX_AMP * math.sin(ang); yb = _HX_YC - _HX_AMP * math.sin(ang)
237
- kind = "user" if start + i < user_len else "gen"
238
- out.append(f"<line class='hx-rung {kind}' x1='{x:.1f}' y1='{yt:.1f}' x2='{x:.1f}' y2='{yb:.1f}'/>")
239
- for i in range(n):
240
- x = i * _HX_SP + _HX_SP / 2
241
- ang = (i + 0.5) * 2 * math.pi / _HX_TURN
242
- yt = _HX_YC + _HX_AMP * math.sin(ang); yb = _HX_YC - _HX_AMP * math.sin(ang)
243
- b = bases[i]; comp = _COMP.get(b, "N")
244
- gen = start + i >= user_len
245
- col = _BASE_COL.get(b, "#999") if gen else _USER_COL
246
- ccol = _BASE_COL.get(comp, "#999") if gen else _USER_COL
247
- op = "1" if gen else ".6"
248
- out.append(f"<text class='hx-base' x='{x:.1f}' y='{yt:.1f}' style='fill:{col};opacity:{op}'>{b}</text>")
249
- out.append(f"<text class='hx-base' x='{x:.1f}' y='{yb:.1f}' style='fill:{ccol};opacity:{op}'>{comp}</text>")
250
- out.append("</svg>")
251
- return "".join(out)
252
-
253
-
254
- def _helix(prompt, gen, accent, live=False):
255
- bases = [c for c in (prompt + gen) if c in "ACGTN"]
256
- user_len = len([c for c in prompt if c in "ACGTN"])
257
- rows, i = [], 0
258
- while i < len(bases):
259
- rows.append(_hx_row(bases[i:i + _HX_PERROW], i, user_len, accent))
260
- i += _HX_PERROW
261
- caret = "<span class='caret'></span>" if live else ""
262
- legend = ("<div class='hx-legend'>"
263
- + "".join(f"<span><span class='hx-dot' style='background:{_BASE_COL[b]}'></span>{b}</span>" for b in "ACGT")
264
- + f"<span><span class='hx-dot' style='background:{_USER_COL}'></span>your input</span></div>")
265
- return f"<div class='helixwrap'>{''.join(rows) or '&nbsp;'}{caret}</div>{legend}"
266
-
267
-
268
- def _seq_track(gen):
269
- """Per-base colored track of the generated bases (our take on the tokenization demo)."""
270
- cells = "".join(f"<span class='seqb {c}'>{c}</span>" for c in gen if c in "ACGT")
271
- return f"<div class='seqtrack'>{cells or '&nbsp;'}</div>"
272
-
273
-
274
- def _kmer_strip(gen):
275
- """The generated sequence cut into our model's actual non-overlapping 6-mer tokens."""
276
- s = "".join(c for c in gen if c in "ACGTN")
277
- toks = [s[i:i + 6] for i in range(0, len(s) - len(s) % 6, 6)]
278
- chips = "".join(f"<span class='tok kmer'>{t}</span>" for t in toks)
279
- head = ("<div class='tkrow'><span class='tklabel'>6-mer tokens</span>"
280
- f"<span class='tkstat'>tokens <span class='n'>{len(toks)}</span></span>"
281
- f"<span class='tkstat'>bases <span class='n'>{len(s)}</span></span>"
282
- "<span class='tkstat'>6 bases / token</span></div>")
283
- return head + f"<div class='tokrow'>{chips}</div>"
284
-
285
-
286
- FOOTER = ("Four ~74M DNA/RNA specialists (β‰ˆ295M total, <b>under Carbon-500M</b>), each distilled "
287
- "per-domain from Carbon-500M. A learned router reads every specialist's surprise + hidden "
288
- "state and routes to the home specialist β€” held-out routing accuracy <b>99.8%</b>. Only one "
289
- "specialist runs per query (~7Γ— cheaper than the 500M monolith).")
290
-
291
-
292
- # ---- handler ----------------------------------------------------------------------
293
- @_gpu(duration=120)
294
- def route_run(seq, n_bases, do_gen, decode="auto"):
295
- yield _notice("Routing & generating")
296
- seq = (seq or "").strip()
297
- if len(seq) < 18:
298
- yield _msg("🧬 Enter a DNA sequence", "Paste at least 18 bases (A/C/G/T) β€” try an example below.")
299
- return
300
- dc = _moe()
301
- doms = dc.domains
302
- bpb = {d: None for d in doms}
303
- # progressively reveal each specialist's surprise (the chain lighting up)
304
- sc, hd = dc._scores_hidden(seq)
305
- for d in doms:
306
- bpb[d] = sc[d] / 6 / 0.6931
307
- yield _wrap("<div class='h'>πŸ”— Sending the sequence down the chain…</div>" + _cards(bpb))
308
- home, _ = dc.route(seq)
309
- c = COLOR.get(home, "#9b59b6")
310
- head = (f"<div class='h'>🧭 Routed to <span style='color:{c}'>{EMOJI.get(home, home)}</span>"
311
- f" β€” the specialist most at home with your sequence</div>" + _cards(bpb, winner=home))
312
- if do_gen:
313
- # decoding: explicit override, else "auto" = argmax for the repeat-structured domains,
314
- # sampled for mrna (a coding domain that stutters into homopolymers under argmax).
315
- dl = (decode or "").lower()
316
- if "sampl" in dl:
317
- greedy = False
318
- elif "argmax" in dl:
319
- greedy = True
320
- else:
321
- greedy = (home != "mrna")
322
- feed_ctx = seq # FULL context to the specialist (it frame-aligns + caps)
323
- ctx = seq[-48:] # short context shown in the helix / text box
324
- mode = "argmax Β· best guess" if greedy else "sampled Β· exploratory"
325
- hxhead = (f"<div class='h'>🧬 {EMOJI.get(home, home)} β€” building the strand base-by-base "
326
- f"<span style='color:var(--muted)'>({mode})</span></div>")
327
- rawhdr = "<div class='h'>raw sequence β€” select to copy</div>"
328
- for gen in dc.generate_stream(home, length=int(n_bases), temperature=1.0, top_k=40,
329
- repetition_penalty=1.3, prompt=feed_ctx, greedy=greedy):
330
- yield _wrap(head + hxhead + _helix(ctx, gen, c, live=True)
331
- + _seq_track(gen) + _kmer_strip(gen)
332
- + rawhdr + _gen_box(ctx, gen, live=True))
333
- _WARMED["done"] = True
334
- gennote = ("<div class='sub'>πŸ§ͺ <b>Generation is exploratory</b> β€” these ~74M specialists are "
335
- "trained on a slice of the corpus, so sampled DNA is low-complexity (and splice / "
336
- "bacterial domains are genuinely AT-rich). The <b>routing</b> and per-base likelihood "
337
- "are the result here, not Carbon-level generation.</div>")
338
- yield _wrap(head + hxhead + _helix(ctx, gen, c, live=False)
339
- + _seq_track(gen) + _kmer_strip(gen)
340
- + rawhdr + _gen_box(ctx, gen, live=False)
341
- + gennote + f"<div class='sub'>{FOOTER}</div>")
342
- else:
343
- _WARMED["done"] = True
344
- yield _wrap(head + f"<div class='sub'>{FOOTER}</div>")
345
-
346
-
347
- STATS_HTML = _wrap(
348
- "<div class='h'>πŸ“Š DaisyChain vs Carbon-500M β€” the fair baseline</div>"
349
- "<div class='stats'>"
350
- "<div class='stat'><div class='v' style='color:#37b24d'>99.8%</div>"
351
- "<div class='l'>routing accuracy<br>(held-out)</div></div>"
352
- "<div class='stat'><div class='v' style='color:#7c5cff'>β‰ˆ295M</div>"
353
- "<div class='l'>total params<br>(4 Γ— ~74M) &lt; Carbon-500M</div></div>"
354
- "<div class='stat'><div class='v' style='color:#22b8cf'>~7Γ—</div>"
355
- "<div class='l'>cheaper per query<br>(one 74M specialist active)</div></div>"
356
- "</div>"
357
- "<table><tr><th>metric</th><th>DaisyChain</th><th>Carbon-500M</th></tr>"
358
- "<tr><td>Likelihood β€” bits/base (↓ better)</td><td class='n'>1.79</td><td class='n'>1.75</td></tr>"
359
- "<tr><td>Seq-recovery, eukaryote (↑ better)</td><td class='n'>31.0%</td><td class='n'>42.2%</td></tr>"
360
- "<tr><td>Seq-recovery, bacteria (↑ better)</td><td class='n'>36.5%</td><td class='n'>49.5%</td></tr>"
361
- "</table>"
362
- "<div class='sub'>Four ~74M specialists (β‰ˆ295M total, <b>under Carbon-500M</b>); only one runs per "
363
- "query, so it's ~7Γ— cheaper per token. Behind the 500M / 1T-token monolith but within striking "
364
- "distance β€” the gap is concentrated in the structured domains (mRNA, bacteria) and keeps closing "
365
- "with more per-domain training. Same protocols as Carbon's eval suite (sequence recovery; per-base "
366
- "likelihood). Carbon-500M is the right yardstick for a sub-500M modular set, not the 3B flagship.</div>")
367
-
368
-
369
- BANNER = _CSS + """
370
- <div class='dcx'><div class='dc-banner'><div class='dc-binner'>
371
- <div class='dc-ident'>
372
- <span class='dc-mark'>🌼</span>
373
- <div>
374
- <div class='dc-title'>DAISYCHAIN</div>
375
- <div class='dc-path'>DAISYCHAINAI / GENOMICS Β· ROUTED DNA SPECIALISTS</div>
376
- </div>
377
- </div>
378
- <div class='dc-word'>DaisyChain<span class='dot'>.</span></div>
379
- <div class='dc-tag'>A modular genomic mind. Four dense ~74M DNA/RNA specialists
380
- (&approx;295M total, <b>under Carbon-500M</b>), each distilled per-domain from Carbon-500M.
381
- A learned router reads how <i>surprised</i> each specialist is by your sequence
382
- (bits/base) plus its hidden state, then hands the work to its home specialist β€”
383
- held-out routing accuracy <b>99.8%</b>. Watch it route in real time.</div>
384
- <div class='dc-motif'>
385
- <span class='dc-chip'>🧬 Eukaryote</span><span class='dc-tie'>β€”</span>
386
- <span class='dc-chip'>🦠 Prokaryote</span><span class='dc-tie'>β€”</span>
387
- <span class='dc-chip'>πŸ“œ mRNA</span><span class='dc-tie'>β€”</span>
388
- <span class='dc-chip'>βœ‚οΈ mRNA-splice</span>
389
- </div>
390
- </div></div></div>"""
391
-
392
- # Light editorial chrome for the Gradio shell so the cream paper extends to the whole page.
393
- # We pin Gradio's theme CSS variables (for BOTH light and .dark) to the paper palette so the
394
- # text never renders white-on-cream when a visitor's browser/Space defaults to dark mode.
395
- _PAGE_CSS = """
396
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&family=JetBrains+Mono:wght@400;500;700&display=swap');
397
- .gradio-container, .gradio-container.dark, .dark, body, body.dark{
398
- --body-background-fill:#f7f5ee!important;
399
- --background-fill-primary:#f7f5ee!important;
400
- --background-fill-secondary:#f2efe2!important;
401
- --block-background-fill:#fbfaf4!important;
402
- --block-label-background-fill:#f2efe2!important;
403
- --input-background-fill:#fffdf6!important;
404
- --body-text-color:#1f1f1d!important;
405
- --body-text-color-subdued:#5a5a55!important;
406
- --block-title-text-color:#1f1f1d!important;
407
- --block-label-text-color:#5a5a55!important;
408
- --block-info-text-color:#5a5a55!important;
409
- --border-color-primary:#d6d3c4!important;
410
- --neutral-50:#f7f5ee!important;
411
- --table-even-background-fill:#fbfaf4!important;
412
- --table-odd-background-fill:#f2efe2!important;
413
- --table-row-focus:#eef3e9!important;
414
- --table-text-color:#1f1f1d!important;
415
- background:#f7f5ee!important;
416
- color:#1f1f1d!important;
417
- }
418
- /* Examples dataset table rows (were rendering black in dark mode) */
419
- .gradio-container .gr-samples-table, .gradio-container [class*='dataset'] table,
420
- .gradio-container [class*='dataset'] td, .gradio-container [class*='dataset'] tr,
421
- .gradio-container [class*='dataset'] tbody{background:#fbfaf4!important;color:#1f1f1d!important}
422
- .gradio-container [class*='dataset'] tr:nth-child(even) td{background:#f2efe2!important}
423
- /* Radio / checkbox options (were rendering black in dark mode) */
424
- .gradio-container [class*='radio'] label, .gradio-container fieldset label,
425
- .gradio-container [class*='checkbox'] label, .gradio-container .wrap label{
426
- background:#fbfaf4!important;color:#1f1f1d!important;border:1px solid #d6d3c4!important}
427
- .gradio-container [class*='radio'] label *, .gradio-container fieldset label *,
428
- .gradio-container [class*='checkbox'] label *{color:#1f1f1d!important}
429
- .gradio-container input[type=radio],.gradio-container input[type=checkbox]{accent-color:#317f3f!important}
430
- .gradio-container{font-family:"Inter","Helvetica Neue",sans-serif!important;max-width:1080px!important}
431
- /* force any Gradio-rendered label / markdown / example text to ink, never white */
432
- .gradio-container label, .gradio-container .prose, .gradio-container p,
433
- .gradio-container span, .gradio-container td, .gradio-container th,
434
- .gradio-container .gr-text-input, .gradio-container input, .gradio-container textarea{color:#1f1f1d!important}
435
- .gradio-container input::placeholder, .gradio-container textarea::placeholder{color:#9c9989!important}
436
- footer{display:none!important}
437
- .gr-button-primary, button.primary{background:#317f3f!important;border:1px solid #2a5931!important;color:#f7f5ee!important;
438
- font-family:"JetBrains Mono",monospace!important;letter-spacing:.08em!important;text-transform:uppercase!important;font-size:12px!important}
439
- .gr-button-primary:hover, button.primary:hover{background:#2a5931!important}
440
- .gr-button-primary *, button.primary *{color:#f7f5ee!important}
441
- """
442
-
443
-
444
- def build():
445
- theme = gr.themes.Default(primary_hue="green", neutral_hue="stone",
446
- font=[gr.themes.GoogleFont("Inter"), "sans-serif"],
447
- font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "monospace"])
448
- # force the light palette so text is never white-on-cream if a visitor defaults to dark mode
449
- _force_light = ("() => { const u = new URL(window.location.href);"
450
- " if (u.searchParams.get('__theme') !== 'light') {"
451
- " u.searchParams.set('__theme','light'); window.location.replace(u.href); } }")
452
- with gr.Blocks(title="DaisyChain β€” modular genomic mind", theme=theme, css=_PAGE_CSS,
453
- js=_force_light) as demo:
454
- gr.HTML(BANNER)
455
- with gr.Row():
456
- seq = gr.Textbox(label="DNA SEQUENCE", lines=3, scale=4,
457
- placeholder="ACGT… (eukaryotic, bacterial, mRNA, or splice-site DNA)")
458
- n = gr.Slider(60, 300, value=90, step=30, label="GENERATE BASES", scale=1)
459
- with gr.Row():
460
- gen_ck = gr.Checkbox(value=True, label="stream a continuation from the routed specialist")
461
- decode = gr.Radio(["sampled (recommended)", "greedy (argmax)"], value="sampled (recommended)",
462
- label="DECODING", scale=1,
463
- info="sampled = varied output (the realistic best for these small models); argmax = deterministic, accurate for the very next bases but collapses into repeats over a full generation")
464
- btn = gr.Button("πŸ”— Route through the DaisyChain", variant="primary")
465
- out = gr.HTML(_wrap("<div class='h'>The chain Β· paste a sequence to light it up</div>"
466
- + _cards({d: None for d in DaisyChain.DESCRIPTIONS})))
467
- btn.click(route_run, [seq, n, gen_ck, decode], out)
468
- try:
469
- ex = json.load(open(os.path.join(HERE, "examples.json")))
470
- gr.Examples([[v, 90, True, "sampled (recommended)"] for v in ex.values()],
471
- inputs=[seq, n, gen_ck, decode],
472
- label="Example sequences (one per domain)")
473
- except Exception:
474
- pass
475
- gr.HTML(STATS_HTML)
476
- return demo
477
-
478
-
479
- if __name__ == "__main__":
480
- build().launch()
 
1
+ """
2
+ DaisyChain β€” interactive routing demo (HuggingFace Space).
3
+
4
+ Paste DNA; the learned router reads how *surprised* each ~74M specialist is (bits/base)
5
+ plus its hidden state and hands the sequence to its home specialist β€” then that specialist
6
+ streams a continuation live. Styled after the Modular-Mind panel: animated routing cards,
7
+ a first-run loading notice, live token streaming. Every handler is a generator.
8
+ """
9
+ import html as _h
10
+ import os
11
+ import json
12
+ import math
13
+
14
+ import gradio as gr
15
+
16
+ # ZeroGPU: @spaces.GPU allocates a GPU only for the decorated call. Falls back to a no-op
17
+ # decorator when `spaces` isn't installed (local / plain CPU).
18
+ try:
19
+ import spaces
20
+ _gpu = spaces.GPU
21
+ except Exception:
22
+ def _gpu(fn=None, **kw):
23
+ return fn if callable(fn) else (lambda f: f)
24
+
25
+ from daisychain import DaisyChain
26
+
27
+ HERE = os.path.dirname(os.path.abspath(__file__))
28
+ MODEL_REPO = os.environ.get("DAISYCHAIN_REPO", "DaisyChainAI/daisychain-genomics")
29
+ DEVICE = os.environ.get("DAISYCHAIN_DEVICE", "cpu")
30
+
31
+ # code + tokenizer + router are bundled here; pull the big specialist weights from the
32
+ # (gated) model repo on first launch using the HF_TOKEN Space secret. No silent
33
+ # swallow β€” if the download fails we want a visible error, not a broken-but-running app.
34
+ if not os.path.exists(os.path.join(HERE, "eukaryote", "model.safetensors")):
35
+ from huggingface_hub import snapshot_download
36
+ snapshot_download(MODEL_REPO, local_dir=HERE,
37
+ token=os.environ.get("HF_TOKEN"),
38
+ allow_patterns=["*/model.safetensors", "tokenizer.json", "router2.pt"])
39
+
40
+ _DC = {"m": None} # lazy-loaded so CUDA is never touched at import
41
+ _WARMED = {"done": False} # so the "loading" notice only shows on the first run
42
+
43
+ EMOJI = {"eukaryote": "🧬 Eukaryote", "prokaryote": "🦠 Prokaryote",
44
+ "mrna": "πŸ“œ mRNA", "mrna_splice": "βœ‚οΈ mRNA-splice"}
45
+ # deeper, paper-friendly tones (vs the old neon dark-theme hues)
46
+ COLOR = {"eukaryote": "#5b4bb0", "prokaryote": "#1f7a99",
47
+ "mrna": "#b03a63", "mrna_splice": "#317f3f"}
48
+ DESC = DaisyChain.DESCRIPTIONS
49
+
50
+
51
+ def _moe():
52
+ if _DC["m"] is None:
53
+ _DC["m"] = DaisyChain(root=HERE, device=DEVICE)
54
+ return _DC["m"]
55
+
56
+
57
+ # ---- HTML rendering ----------------------------------------------------------------
58
+ # Editorial "scientific-paper" aesthetic borrowed from Carbon's demo (cream paper, ink,
59
+ # green accent, mono uppercase headers) but made our own: a daisy-gold second accent and
60
+ # the chain-of-specialists motif. Tokens live in :root so the cards/bars all stay in sync.
61
+ _CSS = """<style>
62
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&family=JetBrains+Mono:wght@400;500;700&display=swap');
63
+ :root{--paper:#f7f5ee;--paper2:#f2efe2;--ink:#1f1f1d;--muted:#8c918b;--hairline:#d6d3c4;
64
+ --green:#317f3f;--green2:#2a5931;--gold:#c79a2e;--gold2:#9c7714}
65
+ .dcx{font-family:"Inter","Helvetica Neue",sans-serif;font-weight:300;color:var(--ink);
66
+ font-size:13px;line-height:1.7;margin:4px 0}
67
+ .dcx .mono{font-family:"JetBrains Mono",ui-monospace,monospace}
68
+ .dcx .note{background:var(--paper2);border:1px solid var(--hairline);border-radius:4px;
69
+ padding:12px 14px;color:#5a5a55;font-size:13px}
70
+ .dcx .h{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:12px;font-weight:500;
71
+ letter-spacing:.16em;text-transform:uppercase;color:var(--ink);margin:6px 0 12px;
72
+ padding-bottom:8px;border-bottom:1px solid var(--hairline)}
73
+ .dcx .p{color:var(--muted)}
74
+ .dcx .g{color:var(--green2);font-weight:600}
75
+ .dcx .chain{display:flex;gap:0;align-items:stretch;flex-wrap:wrap;margin:8px 0}
76
+ .dcx .link{align-self:center;color:var(--hairline);font-size:18px;margin:0 2px 22px;font-weight:700}
77
+ .dcx .card{flex:1;min-width:180px;background:var(--paper);border:1px solid var(--hairline);
78
+ border-radius:4px;padding:12px 13px;position:relative;overflow:hidden;transition:box-shadow .3s,border-color .3s}
79
+ .dcx .card .nm{font-family:"JetBrains Mono",ui-monospace,monospace;font-weight:500;font-size:12px;
80
+ letter-spacing:.06em;text-transform:uppercase}
81
+ .dcx .card .meta{color:var(--muted);font-size:11px;margin-top:4px;min-height:28px;line-height:1.45}
82
+ .dcx .card .bar{height:6px;background:var(--paper2);border:1px solid var(--hairline);border-radius:99px;margin-top:9px;overflow:hidden}
83
+ .dcx .card .fill{height:100%;border-radius:99px;animation:dcxw .7s ease}
84
+ .dcx .card .pct{font-size:10px;color:var(--muted);margin-top:5px;letter-spacing:.04em;text-transform:uppercase;font-family:"JetBrains Mono",monospace}
85
+ .dcx .badge{position:absolute;top:9px;right:10px;font-family:"JetBrains Mono",monospace;font-size:9px;
86
+ font-weight:700;letter-spacing:.12em;padding:3px 8px;border-radius:99px;color:var(--paper);background:var(--green)}
87
+ @keyframes dcxw{from{width:0}}
88
+ .dcx .gen{background:var(--paper2);border:1px solid var(--hairline);border-radius:4px;padding:13px 15px;
89
+ margin:12px 0;font-size:14px;line-height:1.8;font-family:"JetBrains Mono",ui-monospace,monospace;word-break:break-all}
90
+ .dcx .caret{display:inline-block;width:8px;height:16px;border-radius:1px;background:var(--green);
91
+ margin-left:2px;vertical-align:text-bottom;animation:dcxb .8s steps(1) infinite}
92
+ @keyframes dcxb{50%{opacity:0}}
93
+ .dcx .sub{color:var(--muted);font-size:12px;line-height:1.6;margin-top:10px}
94
+ .dcx .stats{display:flex;gap:0;flex-wrap:wrap;margin:12px 0;border:1px solid var(--hairline);border-radius:4px;overflow:hidden}
95
+ .dcx .stat{flex:1;min-width:120px;text-align:center;background:var(--paper);padding:16px 10px;border-right:1px solid var(--hairline)}
96
+ .dcx .stat:last-child{border-right:none}
97
+ .dcx .stat .v{font-family:"JetBrains Mono",monospace;font-size:26px;font-weight:700;line-height:1;color:var(--green2)}
98
+ .dcx .stat .l{font-size:10px;color:var(--muted);margin-top:8px;letter-spacing:.06em;text-transform:uppercase;line-height:1.4}
99
+ .dcx table{border-collapse:collapse;width:100%;margin:10px 0;font-size:12.5px}
100
+ .dcx th,.dcx td{border:1px solid var(--hairline);padding:8px 11px;text-align:left}
101
+ .dcx th{background:var(--paper2);color:var(--ink);font-family:"JetBrains Mono",monospace;font-weight:500;
102
+ font-size:10px;letter-spacing:.1em;text-transform:uppercase}
103
+ .dcx td.n{text-align:right;font-variant-numeric:tabular-nums;font-family:"JetBrains Mono",monospace}
104
+ /* --- live 2D double-helix (our own take on a base-by-base DNA viz) --- */
105
+ .dcx .helixwrap{background:var(--paper2);border:1px solid var(--hairline);border-radius:4px;padding:8px 12px;margin:12px 0;overflow-x:auto}
106
+ .dcx .helix-svg{display:block;margin:1px 0}
107
+ .dcx .hx-strand{fill:none;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;opacity:.5}
108
+ .dcx .hx-strand.back{opacity:.24}
109
+ .dcx .hx-rung{stroke:var(--hairline);stroke-width:1}
110
+ .dcx .hx-rung.user{stroke:#e6e3d6}
111
+ .dcx .hx-base{font-family:"JetBrains Mono",monospace;font-size:9px;font-weight:700;text-anchor:middle;dominant-baseline:central}
112
+ .dcx .hx-legend{display:flex;gap:13px;flex-wrap:wrap;font-family:"JetBrains Mono",monospace;font-size:9px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);margin:2px 0 4px}
113
+ .dcx .hx-dot{display:inline-block;width:8px;height:8px;border-radius:2px;margin-right:5px;vertical-align:middle}
114
+ /* --- tokenization tracks (our take on carbon-tokenization-02): per-base + 6-mer --- */
115
+ .dcx .seqtrack{font-family:"JetBrains Mono",monospace;font-size:11px;background:var(--paper);border:1px solid var(--hairline);border-radius:4px;padding:8px 10px;display:flex;flex-wrap:wrap;gap:1px;margin:8px 0 2px}
116
+ .dcx .seqb{display:inline-flex;align-items:center;justify-content:center;width:18px;height:20px;border-radius:2px;color:#fff;font-weight:500}
117
+ .dcx .seqb.A{background:#1A7A40}.dcx .seqb.T{background:#b00020}.dcx .seqb.C{background:#2c5aa0}.dcx .seqb.G{background:#b8862c}
118
+ .dcx .tkrow{display:flex;gap:14px;align-items:baseline;margin:12px 0 2px;flex-wrap:wrap}
119
+ .dcx .tklabel{font-family:"JetBrains Mono",monospace;font-size:9.5px;font-weight:500;color:#5b5b56;text-transform:uppercase;letter-spacing:1.6px}
120
+ .dcx .tkstat{font-family:"JetBrains Mono",monospace;font-size:10px;color:var(--muted);letter-spacing:.5px}
121
+ .dcx .tkstat .n{font-weight:600;color:var(--green2)}
122
+ .dcx .tokrow{display:flex;flex-wrap:wrap;align-items:center;gap:0}
123
+ .dcx .tok{display:inline-flex;align-items:center;font-family:"JetBrains Mono",monospace;font-size:11px;letter-spacing:.5px;padding:4px 8px;margin:2px;border:1px solid #ccc;border-radius:3px;background:#fff;color:var(--ink)}
124
+ .dcx .tok.kmer{background:rgba(49,127,63,.10);border-color:rgba(49,127,63,.5);color:var(--green2);font-weight:500}
125
+ /* --- editorial hero banner (dotted texture + faint green edge stripes) --- */
126
+ .dc-banner{position:relative;overflow:hidden;border:1px solid var(--hairline);border-radius:6px;margin:2px 0 6px;
127
+ background:radial-gradient(circle at 22% 32%,rgba(0,0,0,.06),transparent 1px),
128
+ radial-gradient(circle at 78% 64%,rgba(0,0,0,.055),transparent 1px),
129
+ linear-gradient(90deg,rgba(49,127,63,.04),transparent 32%,transparent 68%,rgba(199,154,46,.05)),
130
+ var(--paper);
131
+ background-size:7px 7px,11px 11px,auto,auto}
132
+ .dc-binner{position:relative;padding:22px 26px;font-family:"JetBrains Mono",ui-monospace,monospace}
133
+ .dc-ident{display:flex;align-items:center;gap:11px;margin-bottom:16px}
134
+ .dc-mark{font-size:30px;line-height:1}
135
+ .dc-title{font-size:13px;font-weight:700;letter-spacing:.2em;text-transform:uppercase;color:var(--ink)}
136
+ .dc-path{font-size:10px;font-weight:400;letter-spacing:.18em;text-transform:uppercase;color:var(--muted);margin-top:2px}
137
+ .dc-word{font-family:"JetBrains Mono",monospace;font-size:40px;font-weight:700;letter-spacing:-.01em;color:var(--ink);line-height:1.05;margin:4px 0 6px}
138
+ .dc-word .dot{color:var(--gold)}
139
+ .dc-tag{font-family:"Inter",sans-serif;font-weight:300;font-size:13px;color:#5a5a55;max-width:560px;line-height:1.6}
140
+ .dc-motif{display:flex;align-items:center;gap:4px;flex-wrap:wrap;margin-top:18px}
141
+ .dc-chip{font-family:"JetBrains Mono",monospace;font-size:10px;letter-spacing:.06em;text-transform:uppercase;
142
+ background:var(--paper2);border:1px solid var(--hairline);border-radius:99px;padding:5px 11px;color:var(--ink)}
143
+ .dc-tie{color:var(--green);font-size:13px;font-weight:700}
144
+ .dc-rule{height:1px;background:var(--hairline);margin:14px 0}
145
+ </style>"""
146
+
147
+
148
+ def _wrap(body):
149
+ return _CSS + "<div class='dcx'>" + body + "</div>"
150
+
151
+
152
+ def _esc(s):
153
+ return _h.escape(s or "").replace("\n", "<br>")
154
+
155
+
156
+ def _notice(action="Routing"):
157
+ if not _WARMED["done"]:
158
+ try:
159
+ gr.Info("First run β€” loading the four ~74M specialists (~20–40s on CPU). After this it's quick.")
160
+ except Exception:
161
+ pass
162
+ return _wrap(f"<div class='note'>⏳ Loading the four ~74M specialists + {action.lower()}… "
163
+ "first run can take ~20–40s on CPU; every run after is fast.</div>")
164
+ return _wrap(f"<div class='note'>⏳ {action}…</div>")
165
+
166
+
167
+ def _msg(title, body):
168
+ return _wrap(f"<div class='note'><b>{title}</b><br>{body}</div>")
169
+
170
+
171
+ def _cards(bpb, winner=None):
172
+ """One animated card per specialist: surprise (bits/base), confidence bar, winner badge + glow.
173
+ bpb values may be None (not computed yet). Lower bits/base = more 'at home' = fuller bar."""
174
+ cells = []
175
+ doms = list(bpb.keys())
176
+ for i, n in enumerate(doms):
177
+ c = COLOR.get(n, "#9b59b6")
178
+ v = bpb[n]
179
+ win = (n == winner)
180
+ conf = max(0.0, min(1.0, (2.02 - v) / 0.5)) if v is not None else 0.0 # ~1.52..2.02 -> 1..0
181
+ style = f"border-color:{c};box-shadow:0 0 16px {c}40" if win else ""
182
+ badge = f"<span class='badge' style='background:{c}'>ROUTED βœ“</span>" if win else ""
183
+ meta = (f"{DESC.get(n,'')}<br>{v:.3f} bits/base (lower = more at home)"
184
+ if v is not None else f"{DESC.get(n,'')}<br>…")
185
+ bar = (f"<div class='bar'><div class='fill' style='width:{conf*100:.1f}%;background:{c}'></div></div>"
186
+ f"<div class='pct'>confidence {conf*100:.0f}%</div>") if v is not None else \
187
+ "<div class='bar'></div><div class='pct'>…</div>"
188
+ cells.append(
189
+ f"<div class='card' style='{style}'>{badge}"
190
+ f"<div class='nm' style='color:{c}'>{EMOJI.get(n, n)}</div>"
191
+ f"<div class='meta'>{meta}</div>{bar}</div>")
192
+ if i < len(doms) - 1:
193
+ cells.append("<div class='link'>β¬­</div>")
194
+ return "<div class='chain'>" + "".join(cells) + "</div>"
195
+
196
+
197
+ def _gen_box(prompt, gen, live=False):
198
+ caret = "<span class='caret'></span>" if live else ""
199
+ return (f"<div class='gen'><span class='p'>{_esc(prompt)}</span>"
200
+ f"<span class='g'>{_esc(gen)}</span>{caret}</div>")
201
+
202
+
203
+ # ---- live 2D double-helix --------------------------------------------------------
204
+ # A base-by-base SVG double-helix (two sine-wave backbones + per-base rungs, each base
205
+ # letter on the top strand with its Watson–Crick complement on the bottom). Built in
206
+ # Python so it streams inside our existing generator; ours = cream palette, our own
207
+ # nucleotide colors, strands tinted by the routed specialist's accent.
208
+ _COMP = {"A": "T", "T": "A", "C": "G", "G": "C", "N": "N"}
209
+ # shared nucleotide palette (matches our tokenization track): A green, T red, C blue, G amber
210
+ _BASE_COL = {"A": "#1A7A40", "T": "#b00020", "C": "#2c5aa0", "G": "#b8862c"}
211
+ _USER_COL = "#bdbaa9"
212
+ _HX_SP, _HX_AMP, _HX_YC, _HX_ROWH, _HX_TURN, _HX_PERROW = 14, 12, 22, 48, 10.5, 46
213
+
214
+
215
+ def _hx_strand(n, sign):
216
+ pts = []
217
+ for s in range(n * 4 + 1):
218
+ t = s / 4
219
+ x = t * _HX_SP + _HX_SP / 2
220
+ ang = (t + 0.5) * 2 * math.pi / _HX_TURN
221
+ y = _HX_YC + sign * _HX_AMP * math.sin(ang)
222
+ pts.append(f"{x:.1f},{y:.1f}")
223
+ return " ".join(pts)
224
+
225
+
226
+ def _hx_row(bases, start, user_len, accent):
227
+ n = len(bases)
228
+ w = n * _HX_SP + 6
229
+ out = [f"<svg class='helix-svg' width='{w}' height='{_HX_ROWH}' "
230
+ f"viewBox='0 0 {w} {_HX_ROWH}' xmlns='http://www.w3.org/2000/svg'>",
231
+ f"<polyline class='hx-strand' style='stroke:{accent}' points='{_hx_strand(n,1)}'/>",
232
+ f"<polyline class='hx-strand back' style='stroke:{accent}' points='{_hx_strand(n,-1)}'/>"]
233
+ for i in range(n):
234
+ x = i * _HX_SP + _HX_SP / 2
235
+ ang = (i + 0.5) * 2 * math.pi / _HX_TURN
236
+ yt = _HX_YC + _HX_AMP * math.sin(ang); yb = _HX_YC - _HX_AMP * math.sin(ang)
237
+ kind = "user" if start + i < user_len else "gen"
238
+ out.append(f"<line class='hx-rung {kind}' x1='{x:.1f}' y1='{yt:.1f}' x2='{x:.1f}' y2='{yb:.1f}'/>")
239
+ for i in range(n):
240
+ x = i * _HX_SP + _HX_SP / 2
241
+ ang = (i + 0.5) * 2 * math.pi / _HX_TURN
242
+ yt = _HX_YC + _HX_AMP * math.sin(ang); yb = _HX_YC - _HX_AMP * math.sin(ang)
243
+ b = bases[i]; comp = _COMP.get(b, "N")
244
+ gen = start + i >= user_len
245
+ col = _BASE_COL.get(b, "#999") if gen else _USER_COL
246
+ ccol = _BASE_COL.get(comp, "#999") if gen else _USER_COL
247
+ op = "1" if gen else ".6"
248
+ out.append(f"<text class='hx-base' x='{x:.1f}' y='{yt:.1f}' style='fill:{col};opacity:{op}'>{b}</text>")
249
+ out.append(f"<text class='hx-base' x='{x:.1f}' y='{yb:.1f}' style='fill:{ccol};opacity:{op}'>{comp}</text>")
250
+ out.append("</svg>")
251
+ return "".join(out)
252
+
253
+
254
+ def _helix(prompt, gen, accent, live=False):
255
+ bases = [c for c in (prompt + gen) if c in "ACGTN"]
256
+ user_len = len([c for c in prompt if c in "ACGTN"])
257
+ rows, i = [], 0
258
+ while i < len(bases):
259
+ rows.append(_hx_row(bases[i:i + _HX_PERROW], i, user_len, accent))
260
+ i += _HX_PERROW
261
+ caret = "<span class='caret'></span>" if live else ""
262
+ legend = ("<div class='hx-legend'>"
263
+ + "".join(f"<span><span class='hx-dot' style='background:{_BASE_COL[b]}'></span>{b}</span>" for b in "ACGT")
264
+ + f"<span><span class='hx-dot' style='background:{_USER_COL}'></span>your input</span></div>")
265
+ return f"<div class='helixwrap'>{''.join(rows) or '&nbsp;'}{caret}</div>{legend}"
266
+
267
+
268
+ def _seq_track(gen):
269
+ """Per-base colored track of the generated bases (our take on the tokenization demo)."""
270
+ cells = "".join(f"<span class='seqb {c}'>{c}</span>" for c in gen if c in "ACGT")
271
+ return f"<div class='seqtrack'>{cells or '&nbsp;'}</div>"
272
+
273
+
274
+ def _kmer_strip(gen):
275
+ """The generated sequence cut into our model's actual non-overlapping 6-mer tokens."""
276
+ s = "".join(c for c in gen if c in "ACGTN")
277
+ toks = [s[i:i + 6] for i in range(0, len(s) - len(s) % 6, 6)]
278
+ chips = "".join(f"<span class='tok kmer'>{t}</span>" for t in toks)
279
+ head = ("<div class='tkrow'><span class='tklabel'>6-mer tokens</span>"
280
+ f"<span class='tkstat'>tokens <span class='n'>{len(toks)}</span></span>"
281
+ f"<span class='tkstat'>bases <span class='n'>{len(s)}</span></span>"
282
+ "<span class='tkstat'>6 bases / token</span></div>")
283
+ return head + f"<div class='tokrow'>{chips}</div>"
284
+
285
+
286
+ FOOTER = ("Four ~74M DNA/RNA specialists (β‰ˆ295M total, <b>under Carbon-500M</b>), each distilled "
287
+ "per-domain from Carbon-500M. A learned router reads every specialist's surprise + hidden "
288
+ "state and routes to the home specialist β€” held-out routing accuracy <b>99.8%</b>. Only one "
289
+ "specialist runs per query (~7Γ— cheaper than the 500M monolith).")
290
+
291
+
292
+ # ---- handler ----------------------------------------------------------------------
293
+ @_gpu(duration=120)
294
+ def route_run(seq, n_bases, do_gen, decode="auto"):
295
+ yield _notice("Routing & generating")
296
+ seq = (seq or "").strip()
297
+ if len(seq) < 18:
298
+ yield _msg("🧬 Enter a DNA sequence", "Paste at least 18 bases (A/C/G/T) β€” try an example below.")
299
+ return
300
+ dc = _moe()
301
+ doms = dc.domains
302
+ bpb = {d: None for d in doms}
303
+ # progressively reveal each specialist's surprise (the chain lighting up)
304
+ sc, hd = dc._scores_hidden(seq)
305
+ for d in doms:
306
+ bpb[d] = sc[d] / 6 / 0.6931
307
+ yield _wrap("<div class='h'>πŸ”— Sending the sequence down the chain…</div>" + _cards(bpb))
308
+ home, _ = dc.route(seq)
309
+ c = COLOR.get(home, "#9b59b6")
310
+ head = (f"<div class='h'>🧭 Routed to <span style='color:{c}'>{EMOJI.get(home, home)}</span>"
311
+ f" β€” the specialist most at home with your sequence</div>" + _cards(bpb, winner=home))
312
+ if do_gen:
313
+ # decoding: default = base-pair (FNS) β€” marginalize the 6-mer softmax to six 4-way base
314
+ # distributions and sample each base, the same factorization Carbon uses. "argmax" forces
315
+ # the deterministic 6-mer best-guess (matches the recovery metric; collapses over long spans).
316
+ dl = (decode or "").lower()
317
+ greedy = "argmax" in dl
318
+ feed_ctx = seq # FULL context to the specialist (it frame-aligns + caps)
319
+ ctx = seq[-48:] # short context shown in the helix / text box
320
+ mode = "argmax Β· best guess" if greedy else "base-pair Β· FNS"
321
+ hxhead = (f"<div class='h'>🧬 {EMOJI.get(home, home)} β€” building the strand base-by-base "
322
+ f"<span style='color:var(--muted)'>({mode})</span></div>")
323
+ rawhdr = "<div class='h'>raw sequence β€” select to copy</div>"
324
+ if greedy:
325
+ stream = dc.generate_stream(home, length=int(n_bases), prompt=feed_ctx, greedy=True)
326
+ else:
327
+ stream = dc.generate_baselevel_stream(home, length=int(n_bases), temperature=1.0,
328
+ top_p=0.9, prompt=feed_ctx)
329
+ for gen in stream:
330
+ yield _wrap(head + hxhead + _helix(ctx, gen, c, live=True)
331
+ + _seq_track(gen) + _kmer_strip(gen)
332
+ + rawhdr + _gen_box(ctx, gen, live=True))
333
+ _WARMED["done"] = True
334
+ gennote = ("<div class='sub'>πŸ§ͺ <b>Generation is exploratory</b> β€” these ~74M specialists are "
335
+ "trained on a slice of the corpus, so sampled DNA is low-complexity (and splice / "
336
+ "bacterial domains are genuinely AT-rich). The <b>routing</b> and per-base likelihood "
337
+ "are the result here, not Carbon-level generation.</div>")
338
+ yield _wrap(head + hxhead + _helix(ctx, gen, c, live=False)
339
+ + _seq_track(gen) + _kmer_strip(gen)
340
+ + rawhdr + _gen_box(ctx, gen, live=False)
341
+ + gennote + f"<div class='sub'>{FOOTER}</div>")
342
+ else:
343
+ _WARMED["done"] = True
344
+ yield _wrap(head + f"<div class='sub'>{FOOTER}</div>")
345
+
346
+
347
+ STATS_HTML = _wrap(
348
+ "<div class='h'>πŸ“Š DaisyChain vs Carbon-500M β€” the fair baseline</div>"
349
+ "<div class='stats'>"
350
+ "<div class='stat'><div class='v' style='color:#37b24d'>99.8%</div>"
351
+ "<div class='l'>routing accuracy<br>(held-out)</div></div>"
352
+ "<div class='stat'><div class='v' style='color:#7c5cff'>β‰ˆ295M</div>"
353
+ "<div class='l'>total params<br>(4 Γ— ~74M) &lt; Carbon-500M</div></div>"
354
+ "<div class='stat'><div class='v' style='color:#22b8cf'>~7Γ—</div>"
355
+ "<div class='l'>cheaper per query<br>(one 74M specialist active)</div></div>"
356
+ "</div>"
357
+ "<table><tr><th>metric</th><th>DaisyChain</th><th>Carbon-500M</th></tr>"
358
+ "<tr><td>Likelihood β€” bits/base (↓ better)</td><td class='n'>1.79</td><td class='n'>1.75</td></tr>"
359
+ "<tr><td>Seq-recovery, eukaryote (↑ better)</td><td class='n'>31.0%</td><td class='n'>42.2%</td></tr>"
360
+ "<tr><td>Seq-recovery, bacteria (↑ better)</td><td class='n'>36.5%</td><td class='n'>49.5%</td></tr>"
361
+ "</table>"
362
+ "<div class='sub'>Four ~74M specialists (β‰ˆ295M total, <b>under Carbon-500M</b>); only one runs per "
363
+ "query, so it's ~7Γ— cheaper per token. Behind the 500M / 1T-token monolith but within striking "
364
+ "distance β€” the gap is concentrated in the structured domains (mRNA, bacteria) and keeps closing "
365
+ "with more per-domain training. Same protocols as Carbon's eval suite (sequence recovery; per-base "
366
+ "likelihood). Carbon-500M is the right yardstick for a sub-500M modular set, not the 3B flagship.</div>")
367
+
368
+
369
+ BANNER = _CSS + """
370
+ <div class='dcx'><div class='dc-banner'><div class='dc-binner'>
371
+ <div class='dc-ident'>
372
+ <span class='dc-mark'>🌼</span>
373
+ <div>
374
+ <div class='dc-title'>DAISYCHAIN</div>
375
+ <div class='dc-path'>DAISYCHAINAI / GENOMICS Β· ROUTED DNA SPECIALISTS</div>
376
+ </div>
377
+ </div>
378
+ <div class='dc-word'>DaisyChain<span class='dot'>.</span></div>
379
+ <div class='dc-tag'>A modular genomic mind. Four dense ~74M DNA/RNA specialists
380
+ (&approx;295M total, <b>under Carbon-500M</b>), each distilled per-domain from Carbon-500M.
381
+ A learned router reads how <i>surprised</i> each specialist is by your sequence
382
+ (bits/base) plus its hidden state, then hands the work to its home specialist β€”
383
+ held-out routing accuracy <b>99.8%</b>. Watch it route in real time.</div>
384
+ <div class='dc-motif'>
385
+ <span class='dc-chip'>🧬 Eukaryote</span><span class='dc-tie'>β€”</span>
386
+ <span class='dc-chip'>🦠 Prokaryote</span><span class='dc-tie'>β€”</span>
387
+ <span class='dc-chip'>πŸ“œ mRNA</span><span class='dc-tie'>β€”</span>
388
+ <span class='dc-chip'>βœ‚οΈ mRNA-splice</span>
389
+ </div>
390
+ </div></div></div>"""
391
+
392
+ # Light editorial chrome for the Gradio shell so the cream paper extends to the whole page.
393
+ # We pin Gradio's theme CSS variables (for BOTH light and .dark) to the paper palette so the
394
+ # text never renders white-on-cream when a visitor's browser/Space defaults to dark mode.
395
+ _PAGE_CSS = """
396
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&family=JetBrains+Mono:wght@400;500;700&display=swap');
397
+ .gradio-container, .gradio-container.dark, .dark, body, body.dark{
398
+ --body-background-fill:#f7f5ee!important;
399
+ --background-fill-primary:#f7f5ee!important;
400
+ --background-fill-secondary:#f2efe2!important;
401
+ --block-background-fill:#fbfaf4!important;
402
+ --block-label-background-fill:#f2efe2!important;
403
+ --input-background-fill:#fffdf6!important;
404
+ --body-text-color:#1f1f1d!important;
405
+ --body-text-color-subdued:#5a5a55!important;
406
+ --block-title-text-color:#1f1f1d!important;
407
+ --block-label-text-color:#5a5a55!important;
408
+ --block-info-text-color:#5a5a55!important;
409
+ --border-color-primary:#d6d3c4!important;
410
+ --neutral-50:#f7f5ee!important;
411
+ --table-even-background-fill:#fbfaf4!important;
412
+ --table-odd-background-fill:#f2efe2!important;
413
+ --table-row-focus:#eef3e9!important;
414
+ --table-text-color:#1f1f1d!important;
415
+ background:#f7f5ee!important;
416
+ color:#1f1f1d!important;
417
+ }
418
+ /* Examples dataset table rows (were rendering black in dark mode) */
419
+ .gradio-container .gr-samples-table, .gradio-container [class*='dataset'] table,
420
+ .gradio-container [class*='dataset'] td, .gradio-container [class*='dataset'] tr,
421
+ .gradio-container [class*='dataset'] tbody{background:#fbfaf4!important;color:#1f1f1d!important}
422
+ .gradio-container [class*='dataset'] tr:nth-child(even) td{background:#f2efe2!important}
423
+ /* Radio / checkbox options (were rendering black in dark mode) */
424
+ .gradio-container [class*='radio'] label, .gradio-container fieldset label,
425
+ .gradio-container [class*='checkbox'] label, .gradio-container .wrap label{
426
+ background:#fbfaf4!important;color:#1f1f1d!important;border:1px solid #d6d3c4!important}
427
+ .gradio-container [class*='radio'] label *, .gradio-container fieldset label *,
428
+ .gradio-container [class*='checkbox'] label *{color:#1f1f1d!important}
429
+ .gradio-container input[type=radio],.gradio-container input[type=checkbox]{accent-color:#317f3f!important}
430
+ .gradio-container{font-family:"Inter","Helvetica Neue",sans-serif!important;max-width:1080px!important}
431
+ /* force any Gradio-rendered label / markdown / example text to ink, never white */
432
+ .gradio-container label, .gradio-container .prose, .gradio-container p,
433
+ .gradio-container span, .gradio-container td, .gradio-container th,
434
+ .gradio-container .gr-text-input, .gradio-container input, .gradio-container textarea{color:#1f1f1d!important}
435
+ .gradio-container input::placeholder, .gradio-container textarea::placeholder{color:#9c9989!important}
436
+ footer{display:none!important}
437
+ .gr-button-primary, button.primary{background:#317f3f!important;border:1px solid #2a5931!important;color:#f7f5ee!important;
438
+ font-family:"JetBrains Mono",monospace!important;letter-spacing:.08em!important;text-transform:uppercase!important;font-size:12px!important}
439
+ .gr-button-primary:hover, button.primary:hover{background:#2a5931!important}
440
+ .gr-button-primary *, button.primary *{color:#f7f5ee!important}
441
+ """
442
+
443
+
444
+ def build():
445
+ theme = gr.themes.Default(primary_hue="green", neutral_hue="stone",
446
+ font=[gr.themes.GoogleFont("Inter"), "sans-serif"],
447
+ font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "monospace"])
448
+ # force the light palette so text is never white-on-cream if a visitor defaults to dark mode
449
+ _force_light = ("() => { const u = new URL(window.location.href);"
450
+ " if (u.searchParams.get('__theme') !== 'light') {"
451
+ " u.searchParams.set('__theme','light'); window.location.replace(u.href); } }")
452
+ with gr.Blocks(title="DaisyChain β€” modular genomic mind", theme=theme, css=_PAGE_CSS,
453
+ js=_force_light) as demo:
454
+ gr.HTML(BANNER)
455
+ with gr.Row():
456
+ seq = gr.Textbox(label="DNA SEQUENCE", lines=3, scale=4,
457
+ placeholder="ACGT… (eukaryotic, bacterial, mRNA, or splice-site DNA)")
458
+ n = gr.Slider(60, 300, value=90, step=30, label="GENERATE BASES", scale=1)
459
+ with gr.Row():
460
+ gen_ck = gr.Checkbox(value=True, label="stream a continuation from the routed specialist")
461
+ decode = gr.Radio(["base-pair (FNS)", "greedy (argmax)"], value="base-pair (FNS)",
462
+ label="DECODING", scale=1,
463
+ info="base-pair (FNS) = Carbon-style: each base sampled from the marginalized per-position distribution (base-pair control, no 6-mer repeat loops); argmax = deterministic best guess (matches recovery, collapses over long spans)")
464
+ btn = gr.Button("πŸ”— Route through the DaisyChain", variant="primary")
465
+ out = gr.HTML(_wrap("<div class='h'>The chain Β· paste a sequence to light it up</div>"
466
+ + _cards({d: None for d in DaisyChain.DESCRIPTIONS})))
467
+ btn.click(route_run, [seq, n, gen_ck, decode], out)
468
+ try:
469
+ ex = json.load(open(os.path.join(HERE, "examples.json")))
470
+ gr.Examples([[v, 90, True, "base-pair (FNS)"] for v in ex.values()],
471
+ inputs=[seq, n, gen_ck, decode],
472
+ label="Example sequences (one per domain)")
473
+ except Exception:
474
+ pass
475
+ gr.HTML(STATS_HTML)
476
+ return demo
477
+
478
+
479
+ if __name__ == "__main__":
480
+ build().launch()