akagabi commited on
Commit
a3eb5dd
·
verified ·
1 Parent(s): 269467f

standard kit demo: research-preview, hero, try-it, on-device

Browse files
index.html CHANGED
The diff for this file is too large to render. See raw diff
 
js/main.js ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { loadTongue, loadNames } from "./tongue.js";
2
+ import { SAMPLES } from "./samples.js";
3
+ import { previewBanner, hero, sectionCard, brandFooter } from "../kit/components/shell.js";
4
+ import { inputPane, outputPane, controlsRow, statusLine } from "../kit/components/panes.js";
5
+ import { mountSamples } from "../kit/components/samples.js";
6
+
7
+ const app = document.getElementById("app");
8
+
9
+ const input = inputPane({ label: "Text", id: "raw-text" });
10
+ const output = outputPane({ label: "Language", placeholder: "the language appears here" });
11
+ const status = statusLine();
12
+
13
+ const chipsLabel = document.createElement("span");
14
+ chipsLabel.className = "chips-label";
15
+ chipsLabel.textContent = "Load a sample";
16
+ const chips = document.createElement("div");
17
+ chips.className = "chips";
18
+ input.root.append(controlsRow(chipsLabel, chips), controlsRow(status.root));
19
+
20
+ const demo = sectionCard({ eyebrow: "Try it · on this device", id: "demo" });
21
+ const grid = document.createElement("div");
22
+ grid.className = "demo-grid";
23
+ grid.append(input.root, output.root);
24
+ demo.append(grid);
25
+
26
+ app.append(
27
+ previewBanner({
28
+ title: "Research preview",
29
+ text: "An early look at tongue, an on-device language identifier. The model runs in this page; nothing you type leaves it.",
30
+ }),
31
+ hero({
32
+ name: "Tongue",
33
+ lead: "Type a few words in any language and it names the language, offline, in a couple of milliseconds.",
34
+ accent: "83 languages, 2 MB, on the device.",
35
+ }),
36
+ demo,
37
+ brandFooter(),
38
+ );
39
+
40
+ mountSamples({ container: chips, textarea: input.textarea, samples: SAMPLES });
41
+
42
+ function esc(s){ return s.replace(/[&<>]/g, c=>({"&":"&amp;","<":"&lt;",">":"&gt;"}[c])); }
43
+
44
+ let model = null, NAMES = {};
45
+
46
+ function render(){
47
+ const text = input.textarea.value;
48
+ if(!text.trim() || !model){ output.out.innerHTML = ""; return; }
49
+ const r = model.detect(text);
50
+ if(r.top.length === 0){ output.out.innerHTML = ""; return; }
51
+ const nameOf = c => NAMES[c] || [c, ""];
52
+ const [nm, native] = nameOf(r.top[0].lang);
53
+ const tentative = r.reliability === "tentative";
54
+ const bars = r.top.map((t, i) => {
55
+ const [n2] = nameOf(t.lang);
56
+ return `<div class="lang-row"><span class="lang-name">${esc(n2)}</span>`
57
+ + `<span class="lang-bar"><span class="lang-fill${i?' alt':''}" style="transform:scaleX(${t.p.toFixed(3)})"></span></span>`
58
+ + `<span class="lang-pct">${(t.p*100).toFixed(1)}%</span></div>`;
59
+ }).join("");
60
+ const lead = tentative
61
+ ? `<p class="lang-note">Short or ambiguous — a best guess only. Add a few words for a confident read.</p>` : "";
62
+ output.out.innerHTML =
63
+ `${lead}<div class="lang-head"><span class="lang-primary${tentative?' soft':''}">${esc(nm)}</span>`
64
+ + `<span class="lang-native">${esc(native)}</span></div><div class="lang-bars">${bars}</div>`;
65
+ requestAnimationFrame(() => output.out.querySelectorAll(".lang-fill").forEach(el => {
66
+ const t = el.style.transform; el.style.transform = "scaleX(0)"; requestAnimationFrame(() => el.style.transform = t);
67
+ }));
68
+ status.set(`${[...r.text].length} characters · on this device`);
69
+ }
70
+
71
+ input.textarea.addEventListener("input", render);
72
+
73
+ status.set("loading the model…", true);
74
+ Promise.all([loadTongue(), loadNames()]).then(([m, n]) => {
75
+ model = m; NAMES = n;
76
+ status.set("ready · nothing leaves this page");
77
+ render();
78
+ }).catch(e => status.set("failed to load: " + e.message));
js/samples.js ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const SAMPLES = [
2
+ { id: "de", label: "kann ich das haben", text: "kann ich das haben" },
3
+ { id: "fr", label: "je voudrais un café", text: "je voudrais un café au lait" },
4
+ { id: "es", label: "¿dónde está la estación?", text: "¿dónde está la estación?" },
5
+ { id: "it", label: "quanto costa il biglietto", text: "quanto costa il biglietto" },
6
+ { id: "ko", label: "안녕하세요", text: "안녕하세요 만나서 반갑습니다" },
7
+ { id: "ja", label: "こんにちは", text: "こんにちは、お元気ですか" },
8
+ { id: "ru", label: "привет как дела", text: "привет как твои дела сегодня" },
9
+ { id: "ar", label: "مرحبا كيف حالك", text: "مرحبا كيف حالك اليوم" },
10
+ { id: "el", label: "Καλημέρα", text: "Καλημέρα τι κάνεις σήμερα" },
11
+ { id: "id", label: "saya suka makan", text: "saya suka makan nasi goreng" },
12
+ { id: "cy", label: "dw i'n hoffi coffi", text: "dw i'n hoffi coffi" },
13
+ { id: "vi", label: "xin chào bạn", text: "xin chào bạn khỏe không" },
14
+ ];
js/tongue.js ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Pure-JS port of the tongue inference pipeline. Must match the Python
2
+ // reference byte for byte on normalize + hashing + script routing, then
3
+ // dequantize the int8 embedding and run the linear head. Verified against
4
+ // Python by scripts/verify_web.py through tests/run_infer.mjs.
5
+
6
+ // ---- normalize (docs/normalizer.md) ----
7
+ const URL_RE = /(?:https?:\/\/|www\.)\S+/giu;
8
+ const EMAIL_RE = /\S+@\S+\.\S+/gu;
9
+ const MENTION_RE = /[@#][\p{L}\p{N}_]+/gu;
10
+ const DIGIT_RE = /\p{Nd}+/gu;
11
+ const DISCARD_RE = /[\p{So}\p{Sk}\p{Cf}\p{Co}\p{Cn}]/gu;
12
+ const WS_RE = /\s+/gu;
13
+ const MAX_CHARS = 512;
14
+
15
+ export function normalize(text) {
16
+ let t = text.normalize("NFC");
17
+ t = t.replace(URL_RE, " ").replace(EMAIL_RE, " ").replace(MENTION_RE, " ");
18
+ t = t.replace(DIGIT_RE, " ").replace(DISCARD_RE, "");
19
+ t = t.toLowerCase();
20
+ t = t.replace(WS_RE, " ").trim();
21
+ return [...t].slice(0, MAX_CHARS).join("");
22
+ }
23
+
24
+ // ---- FNV-1a 32-bit over Unicode scalars (hashing.py) ----
25
+ const OFFSET = 0x811c9dc5, PRIME = 0x01000193, MASK = 0xffffffff;
26
+ function fnv1a(str) {
27
+ let h = OFFSET;
28
+ for (const ch of str) {
29
+ h ^= ch.codePointAt(0);
30
+ h = Math.imul(h, PRIME) & MASK;
31
+ }
32
+ return h >>> 0;
33
+ }
34
+ function* ngrams(text, orders) {
35
+ if (!text) return;
36
+ for (const token of text.split(" ")) {
37
+ if (!token) continue;
38
+ const marked = "^" + token + "$";
39
+ const cps = [...marked];
40
+ for (const n of orders) {
41
+ if (n > cps.length) continue;
42
+ for (let i = 0; i + n <= cps.length; i++) yield cps.slice(i, i + n).join("");
43
+ }
44
+ }
45
+ }
46
+
47
+ // ---- script routing (script.py) ----
48
+ const RANGES = [
49
+ [0x41,0x5a,"Latin"],[0x61,0x7a,"Latin"],[0xc0,0x24f,"Latin"],
50
+ [0x370,0x3ff,"Greek"],[0x1f00,0x1fff,"Greek"],
51
+ [0x400,0x52f,"Cyrillic"],[0x2de0,0x2dff,"Cyrillic"],[0xa640,0xa69f,"Cyrillic"],
52
+ [0x530,0x58f,"Armenian"],[0x590,0x5ff,"Hebrew"],
53
+ [0x600,0x6ff,"Arabic"],[0x750,0x77f,"Arabic"],[0x8a0,0x8ff,"Arabic"],[0xfb50,0xfdff,"Arabic"],[0xfe70,0xfeff,"Arabic"],
54
+ [0x700,0x74f,"Syriac"],[0x780,0x7bf,"Thaana"],
55
+ [0x900,0x97f,"Devanagari"],[0x980,0x9ff,"Bengali"],[0xa00,0xa7f,"Gurmukhi"],[0xa80,0xaff,"Gujarati"],
56
+ [0xb00,0xb7f,"Oriya"],[0xb80,0xbff,"Tamil"],[0xc00,0xc7f,"Telugu"],[0xc80,0xcff,"Kannada"],
57
+ [0xd00,0xd7f,"Malayalam"],[0xd80,0xdff,"Sinhala"],[0xe00,0xe7f,"Thai"],[0xe80,0xeff,"Lao"],
58
+ [0xf00,0xfff,"Tibetan"],[0x1000,0x109f,"Myanmar"],[0x10a0,0x10ff,"Georgian"],[0x2d00,0x2d2f,"Georgian"],
59
+ [0x1200,0x137f,"Ethiopic"],[0x13a0,0x13ff,"Cherokee"],[0x1780,0x17ff,"Khmer"],[0x1800,0x18af,"Mongolian"],
60
+ [0x3040,0x309f,"Hiragana"],[0x30a0,0x30ff,"Katakana"],[0x31f0,0x31ff,"Katakana"],
61
+ [0x3400,0x4dbf,"Han"],[0x4e00,0x9fff,"Han"],[0xf900,0xfaff,"Han"],
62
+ [0xac00,0xd7af,"Hangul"],[0x1100,0x11ff,"Hangul"],[0x3130,0x318f,"Hangul"],
63
+ ].sort((a,b)=>a[0]-b[0]);
64
+ const DECISIVE = {Hangul:"ko",Hiragana:"ja",Katakana:"ja",Thai:"th",Lao:"lo",Khmer:"km",Myanmar:"my",
65
+ Greek:"el",Hebrew:"he",Armenian:"hy",Georgian:"ka",Ethiopic:"am",Cherokee:"chr",Tamil:"ta",Telugu:"te",
66
+ Kannada:"kn",Malayalam:"ml",Sinhala:"si",Gujarati:"gu",Gurmukhi:"pa",Oriya:"or",Thaana:"dv",Tibetan:"bo",
67
+ Mongolian:"mn",Syriac:"syr",Han:"zh"};
68
+ const NARROWING = {Cyrillic:["ru","uk","bg","sr","mk","be","kk","ky"],Arabic:["ar","fa","ur","ug"],
69
+ Devanagari:["hi","mr","ne"],Bengali:["bn","as"]};
70
+ function scriptOf(cp){
71
+ let lo=0,hi=RANGES.length-1,ans=null;
72
+ while(lo<=hi){const m=(lo+hi)>>1; if(RANGES[m][0]<=cp){ans=RANGES[m];lo=m+1;}else hi=m-1;}
73
+ return ans && cp<=ans[1] ? ans[2] : null;
74
+ }
75
+ function histogram(text){const h={};for(const ch of text){const s=scriptOf(ch.codePointAt(0));if(s)h[s]=(h[s]||0)+1;}return h;}
76
+ function presence(hist,names){
77
+ const scripted=Object.values(hist).reduce((a,b)=>a+b,0);
78
+ let best=null,bc=0;
79
+ for(const s of Object.keys(hist))if(names[s]!==undefined&&hist[s]>bc){bc=hist[s];best=s;}
80
+ if(best===null)return null;
81
+ const share=bc/scripted;
82
+ return (share>=0.5||(bc>=2&&share>=0.25))?best:null;
83
+ }
84
+ export function route(text){
85
+ const hist=histogram(text);
86
+ if(Object.keys(hist).length===0)return{verdict:"ambiguous",candidates:[]};
87
+ if(hist.Hiragana||hist.Katakana)return{verdict:"decisive",candidates:["ja"]};
88
+ const d=presence(hist,DECISIVE);
89
+ if(d!==null)return{verdict:"decisive",candidates:[DECISIVE[d]]};
90
+ const n=presence(hist,Object.fromEntries(Object.keys(NARROWING).map(k=>[k,1])));
91
+ if(n!==null)return{verdict:"narrowing",candidates:NARROWING[n]};
92
+ return{verdict:"ambiguous",candidates:[]};
93
+ }
94
+
95
+ // ---- model ----
96
+ export function makeModel(meta, bytes){
97
+ const {num_buckets:B, dim:D, labels, embed_scale:scale, ngram_orders:orders} = meta;
98
+ const emb = new Int8Array(bytes.buffer, bytes.byteOffset, B*D);
99
+ const linOff = B*D;
100
+ const linW = new Float32Array(bytes.buffer.slice(linOff, linOff + labels.length*D*4));
101
+ const linBOff = linOff + labels.length*D*4;
102
+ const linB = new Float32Array(bytes.buffer.slice(linBOff, linBOff + labels.length*4));
103
+ const latin = new Set(meta.latin_labels || labels); // full mask by default
104
+
105
+ function logits(text, maskSet){
106
+ const acc = new Float32Array(D);
107
+ for(const g of ngrams(text, orders)){
108
+ const b = (fnv1a(g) % B) >>> 0, base = b*D;
109
+ for(let k=0;k<D;k++) acc[k] += emb[base+k]*scale;
110
+ }
111
+ const out = new Float32Array(labels.length).fill(-Infinity);
112
+ for(let j=0;j<labels.length;j++){
113
+ if(maskSet && !maskSet.has(labels[j])) continue;
114
+ let s = linB[j], wOff = j*D;
115
+ for(let k=0;k<D;k++) s += linW[wOff+k]*acc[k];
116
+ out[j] = s;
117
+ }
118
+ return out;
119
+ }
120
+ function softmaxTop(out, k=3){
121
+ let mx=-Infinity; for(const v of out) if(v>mx) mx=v;
122
+ let sum=0; const ex=out.map(v=>{const e=v===-Infinity?0:Math.exp(v-mx); sum+=e; return e;});
123
+ const idx=[...ex.keys()].sort((a,b)=>ex[b]-ex[a]).slice(0,k);
124
+ return idx.map(i=>({lang:labels[i], p: sum>0?ex[i]/sum:0}));
125
+ }
126
+ // Short Latin fragments are unreliable for EVERY detector — "hi i am" reads
127
+ // as Welsh to any character model. Reliability keys off evidence (input
128
+ // length + how far the top guess leads the runner-up), not raw softmax
129
+ // confidence, which is overconfident on short input. Script-decided input
130
+ // is always reliable; there's no guessing involved.
131
+ function reliability(text, top, via){
132
+ if(via==="script") return "confident";
133
+ const chars = [...text].length;
134
+ const words = text.split(" ").filter(Boolean).length;
135
+ const margin = top.length>1 ? top[0].p-top[1].p : top[0]?.p??0;
136
+ if(chars>=18 && margin>=0.30) return "confident";
137
+ if(chars>=12 && margin>=0.20) return "likely";
138
+ return "tentative";
139
+ }
140
+ return {
141
+ labels, latin,
142
+ detect(raw){
143
+ const text = normalize(raw);
144
+ if(!text) return {text, top:[], via:"empty", reliability:"empty"};
145
+ const r = route(text);
146
+ if(r.verdict==="decisive") return {text, top:[{lang:r.candidates[0], p:1}], via:"script", reliability:"confident"};
147
+ const maskSet = r.verdict==="narrowing" ? new Set(r.candidates) : latin;
148
+ const usable = [...maskSet].filter(l=>labels.includes(l));
149
+ if(usable.length===0) return {text, top:[], via:"unsupported", reliability:"empty"};
150
+ const via = r.verdict==="narrowing" ? "narrowed" : "model";
151
+ const top = softmaxTop(logits(text, new Set(usable)), 3);
152
+ return {text, top, via, reliability: reliability(text, top, via)};
153
+ }
154
+ };
155
+ }
156
+
157
+ // Load the embedded weights and build a ready model. The weights live in
158
+ // site/models/ alongside the page; nothing is fetched from a server elsewhere.
159
+ export async function loadTongue(base = "./models/"){
160
+ const [meta, b64] = await Promise.all([
161
+ fetch(base+"meta.json").then(r=>r.json()),
162
+ fetch(base+"model.b64").then(r=>r.text()),
163
+ ]);
164
+ const raw = Uint8Array.from(atob(b64.trim()), c=>c.charCodeAt(0));
165
+ const ds = new DecompressionStream("gzip");
166
+ const buf = new Uint8Array(await new Response(new Blob([raw]).stream().pipeThrough(ds)).arrayBuffer());
167
+ return makeModel(meta, buf);
168
+ }
169
+ export async function loadNames(base = "./models/"){ return fetch(base+"names.json").then(r=>r.json()); }
kit/assets/favicon-32.png ADDED
kit/assets/favicon.svg ADDED
kit/assets/fonts/HankenGrotesk-latin-wght.woff2 ADDED
Binary file (34.7 kB). View file
 
kit/assets/fonts/InstrumentSerif-latin-Italic.woff2 ADDED
Binary file (15.7 kB). View file
 
kit/assets/fonts/InstrumentSerif-latin-Regular.woff2 ADDED
Binary file (15 kB). View file
 
kit/assets/fonts/JetBrainsMono-latin-wght.woff2 ADDED
Binary file (31.3 kB). View file
 
kit/assets/fonts/NOTICE ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Font notices for the woff2 subsets in this directory. All three families
2
+ are licensed under the SIL Open Font License, Version 1.1, reproduced in
3
+ full below.
4
+
5
+ HankenGrotesk-latin-wght.woff2
6
+ Copyright 2019 The Hanken Grotesk Project Authors
7
+ (https://github.com/marcologous/hanken-grotesk)
8
+
9
+ InstrumentSerif-latin-Regular.woff2, InstrumentSerif-latin-Italic.woff2
10
+ Copyright 2022 The Instrument Serif Project Authors
11
+ (https://github.com/Instrument/instrument-serif)
12
+
13
+ JetBrainsMono-latin-wght.woff2
14
+ Copyright 2020 The JetBrains Mono Project Authors
15
+ (https://github.com/JetBrains/JetBrainsMono)
16
+
17
+ -----------------------------------------------------------------------
18
+
19
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
20
+
21
+ PREAMBLE
22
+ The goals of the Open Font License (OFL) are to stimulate worldwide
23
+ development of collaborative font projects, to support the font creation
24
+ efforts of academic and linguistic communities, and to provide a free and
25
+ open framework in which fonts may be shared and improved in partnership
26
+ with others.
27
+
28
+ The OFL allows the licensed fonts to be used, studied, modified and
29
+ redistributed freely as long as they are not sold by themselves. The
30
+ fonts, including any derivative works, can be bundled, embedded,
31
+ redistributed and/or sold with any software provided that any reserved
32
+ names are not used by derivative works. The fonts and derivatives,
33
+ however, cannot be released under any other type of license. The
34
+ requirement for fonts to remain under this license does not apply to any
35
+ document created using the fonts or their derivatives.
36
+
37
+ DEFINITIONS
38
+ "Font Software" refers to the set of files released by the Copyright
39
+ Holder(s) under this license and clearly marked as such. This may include
40
+ source files, build scripts and documentation.
41
+
42
+ "Reserved Font Name" refers to any names specified as such after the
43
+ copyright statement(s).
44
+
45
+ "Original Version" refers to the collection of Font Software components
46
+ as distributed by the Copyright Holder(s).
47
+
48
+ "Modified Version" refers to any derivative made by adding to, deleting,
49
+ or substituting -- in part or in whole -- any of the components of the
50
+ Original Version, by changing formats or by porting the Font Software to
51
+ a new environment.
52
+
53
+ "Author" refers to any designer, engineer, programmer, technical writer
54
+ or other person who contributed to the Font Software.
55
+
56
+ PERMISSION & CONDITIONS
57
+ Permission is hereby granted, free of charge, to any person obtaining a
58
+ copy of the Font Software, to use, study, copy, merge, embed, modify,
59
+ redistribute, and sell modified and unmodified copies of the Font
60
+ Software, subject to the following conditions:
61
+
62
+ 1) Neither the Font Software nor any of its individual components, in
63
+ Original or Modified Versions, may be sold by itself.
64
+
65
+ 2) Original or Modified Versions of the Font Software may be bundled,
66
+ redistributed and/or sold with any software, provided that each copy
67
+ contains the above copyright notice and this license. These can be
68
+ included either as stand-alone text files, human-readable headers or in
69
+ the appropriate machine-readable metadata fields within text or binary
70
+ files as long as those fields can be easily viewed by the user.
71
+
72
+ 3) No Modified Version of the Font Software may use the Reserved Font
73
+ Name(s) unless explicit written permission is granted by the
74
+ corresponding Copyright Holder. This restriction only applies to the
75
+ primary font name as presented to the users.
76
+
77
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
78
+ Software shall not be used to promote, endorse or advertise any Modified
79
+ Version, except to acknowledge the contribution(s) of the Copyright
80
+ Holder(s) and the Author(s) or with their explicit written permission.
81
+
82
+ 5) The Font Software, modified or unmodified, in part or in whole, must
83
+ be distributed entirely under this license, and must not be distributed
84
+ under any other license. The requirement for fonts to remain under this
85
+ license does not apply to any document created using the Font Software.
86
+
87
+ TERMINATION
88
+ This license becomes null and void if any of the above conditions are not
89
+ met.
90
+
91
+ DISCLAIMER
92
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
93
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
94
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
95
+ COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
96
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
97
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
98
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
99
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
100
+ DEALINGS IN THE FONT SOFTWARE.
kit/components/panes.js ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { paintSwarm, setSwarm } from "./swarm.js";
2
+
3
+ function paneLabel(text) {
4
+ const row = document.createElement("div");
5
+ row.className = "pane-label";
6
+ const caption = document.createElement("span");
7
+ caption.textContent = text;
8
+ row.append(caption);
9
+ return row;
10
+ }
11
+
12
+ export function controlsRow(...children) {
13
+ const row = document.createElement("div");
14
+ row.className = "controls";
15
+ row.append(...children);
16
+ return row;
17
+ }
18
+
19
+ export function inputPane({ label = "Input", id } = {}) {
20
+ const root = document.createElement("div");
21
+ root.className = "pane";
22
+ const labelRow = paneLabel(label);
23
+ const textarea = document.createElement("textarea");
24
+ if (id) textarea.id = id;
25
+ textarea.spellcheck = false;
26
+ textarea.setAttribute("aria-label", label);
27
+ root.append(labelRow, textarea);
28
+ return { root, labelRow, textarea };
29
+ }
30
+
31
+ export function outputPane({ label = "Output", placeholder = "output appears here" } = {}) {
32
+ const root = document.createElement("div");
33
+ root.className = "pane";
34
+ const labelRow = paneLabel(label);
35
+ const actions = document.createElement("span");
36
+ actions.className = "controls";
37
+ labelRow.append(actions);
38
+ const out = document.createElement("div");
39
+ out.className = "out";
40
+ out.dataset.placeholder = placeholder;
41
+ out.setAttribute("aria-live", "polite");
42
+ out.setAttribute("aria-label", label);
43
+ root.append(labelRow, out);
44
+ return { root, labelRow, actions, out };
45
+ }
46
+
47
+ export function runButton(label) {
48
+ const button = document.createElement("button");
49
+ button.type = "button";
50
+ button.className = "run-btn";
51
+ button.textContent = label;
52
+ return button;
53
+ }
54
+
55
+ export function ghostButton(label) {
56
+ const button = document.createElement("button");
57
+ button.type = "button";
58
+ button.className = "ghost-btn";
59
+ button.textContent = label;
60
+ return button;
61
+ }
62
+
63
+ export function statusLine() {
64
+ const root = document.createElement("span");
65
+ root.className = "status";
66
+ root.setAttribute("role", "status");
67
+ const swarm = document.createElement("span");
68
+ swarm.className = "swarm";
69
+ swarm.setAttribute("aria-hidden", "true");
70
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
71
+ svg.setAttribute("viewBox", "0 0 24 24");
72
+ swarm.append(svg);
73
+ paintSwarm(svg);
74
+ const text = document.createElement("span");
75
+ root.append(swarm, text);
76
+ return {
77
+ root,
78
+ set(message, busy = false) {
79
+ text.textContent = message;
80
+ setSwarm(swarm, busy);
81
+ },
82
+ };
83
+ }
kit/components/samples.js ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function mountSamples({ container, textarea, samples, autofill = true }) {
2
+ const chips = [];
3
+ const clear = () => {
4
+ for (const chip of chips) chip.setAttribute("aria-pressed", "false");
5
+ };
6
+ for (const [index, sample] of samples.entries()) {
7
+ const chip = document.createElement("button");
8
+ chip.type = "button";
9
+ chip.className = "chip";
10
+ chip.textContent = sample.label;
11
+ chip.setAttribute("aria-pressed", autofill && index === 0 ? "true" : "false");
12
+ chip.addEventListener("click", () => {
13
+ textarea.value = sample.text;
14
+ clear();
15
+ chip.setAttribute("aria-pressed", "true");
16
+ });
17
+ chips.push(chip);
18
+ container.append(chip);
19
+ }
20
+ if (autofill && samples.length > 0) textarea.value = samples[0].text;
21
+ textarea.addEventListener("input", clear);
22
+ return chips;
23
+ }
kit/components/shell.js ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function eyebrow(text) {
2
+ const root = document.createElement("div");
3
+ root.className = "eyebrow";
4
+ const tick = document.createElement("span");
5
+ tick.className = "tick";
6
+ tick.textContent = "◆";
7
+ const label = document.createElement("span");
8
+ label.textContent = text;
9
+ root.append(tick, label);
10
+ return root;
11
+ }
12
+
13
+ export function sectionCard({ eyebrow: eyebrowText, id } = {}) {
14
+ const root = document.createElement("section");
15
+ root.className = "card";
16
+ if (id) root.id = id;
17
+ if (eyebrowText) root.append(eyebrow(eyebrowText));
18
+ return root;
19
+ }
20
+
21
+ export function previewBanner({ title = "Research preview", text }) {
22
+ const root = document.createElement("div");
23
+ root.className = "preview-banner";
24
+ root.setAttribute("role", "note");
25
+ const strong = document.createElement("strong");
26
+ strong.textContent = title;
27
+ const span = document.createElement("span");
28
+ span.textContent = text;
29
+ root.append(strong, span);
30
+ return root;
31
+ }
32
+
33
+ export function hero({ eyebrow: eyebrowText = "Desert Ant Labs · on-device models", name, lead, accent }) {
34
+ const root = sectionCard({ eyebrow: eyebrowText });
35
+ const heading = document.createElement("h1");
36
+ heading.className = "display";
37
+ heading.append(document.createTextNode(name));
38
+ const dot = document.createElement("span");
39
+ dot.className = "dot";
40
+ dot.textContent = ".";
41
+ heading.append(dot);
42
+ root.append(heading);
43
+ if (lead || accent) {
44
+ const p = document.createElement("p");
45
+ p.className = "lead";
46
+ if (lead) p.append(document.createTextNode(accent ? `${lead} ` : lead));
47
+ if (accent) {
48
+ const mark = document.createElement("span");
49
+ mark.className = "accent";
50
+ mark.textContent = accent;
51
+ p.append(mark);
52
+ }
53
+ root.append(p);
54
+ }
55
+ return root;
56
+ }
57
+
58
+ export function brandFooter({ note = `Runs local · ${new Date().getFullYear()}` } = {}) {
59
+ const root = document.createElement("div");
60
+ root.className = "brand-footer";
61
+ const left = document.createElement("span");
62
+ left.textContent = "Desert Ant Labs";
63
+ const right = document.createElement("span");
64
+ right.textContent = note;
65
+ root.append(left, right);
66
+ return root;
67
+ }
kit/components/swarm.js ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function paintSwarm(svg) {
2
+ if (!svg || svg.dataset.painted) return;
3
+ const C = 12, R = 8, r = 1.5, count = 8, speed = 1.6;
4
+ const rnd = (i, k) => {
5
+ const v = Math.sin((i + 1) * 12.9898 + k * 78.233) * 43758.5453;
6
+ return v - Math.floor(v);
7
+ };
8
+ let style = "", dots = "";
9
+ for (let i = 0; i < count; i += 1) {
10
+ const a = (i / count) * Math.PI * 2 - Math.PI / 2;
11
+ const x = (C + R * Math.cos(a)).toFixed(2);
12
+ const y = (C + R * Math.sin(a)).toFixed(2);
13
+ const inwardX = C - x, inwardY = C - y;
14
+ const perpX = -(C - y), perpY = C - x;
15
+ const pull = 0.45 + rnd(i, 1) * 0.6;
16
+ const wob = (rnd(i, 2) - 0.5) * 0.5;
17
+ const tx = (inwardX * pull + perpX * wob).toFixed(2);
18
+ const ty = (inwardY * pull + perpY * wob).toFixed(2);
19
+ const mid = (0.45 + rnd(i, 3) * 0.3).toFixed(2);
20
+ const dur = (speed * (0.74 + rnd(i, 4) * 0.62)).toFixed(2);
21
+ const delay = (-rnd(i, 5) * speed * 1.4).toFixed(2);
22
+ dots += `<circle class="dot d${i}" cx="${x}" cy="${y}" r="${r}"></circle>`;
23
+ style += `@keyframes sw${i}{0%{transform:translate(0,0) scale(1);opacity:1}50%{transform:translate(${tx}px,${ty}px) scale(${mid});opacity:.45}100%{transform:translate(0,0) scale(1);opacity:1}}.d${i}{animation:sw${i} ${dur}s cubic-bezier(.45,0,.55,1) ${delay}s infinite}`;
24
+ }
25
+ svg.innerHTML = `<g>${dots}</g><circle class="core" cx="${C}" cy="${C}" r="1.7"></circle><style>${style}</style>`;
26
+ svg.dataset.painted = "1";
27
+ }
28
+
29
+ export function setSwarm(el, on) {
30
+ if (el) el.classList.toggle("visible", !!on);
31
+ }
kit/js/tags.js ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function argmaxRows(logits, seqLength, classCount) {
2
+ const rows = new Array(seqLength);
3
+ for (let pos = 0; pos < seqLength; pos += 1) {
4
+ const base = pos * classCount;
5
+ let best = 0;
6
+ for (let c = 1; c < classCount; c += 1) {
7
+ if (logits[base + c] > logits[base + best]) best = c;
8
+ }
9
+ rows[pos] = best;
10
+ }
11
+ return rows;
12
+ }
13
+
14
+ export function argmaxHeads(outputs, seqLength, heads, { outputName = (head) => `logits_${head}` } = {}) {
15
+ const preds = {};
16
+ for (const [head, labels] of Object.entries(heads)) {
17
+ const classCount = typeof labels === "number" ? labels : labels.length;
18
+ const tensor = outputs[outputName(head)];
19
+ if (!tensor) throw new Error(`missing model output "${outputName(head)}"`);
20
+ preds[head] = argmaxRows(tensor.data, seqLength, classCount);
21
+ }
22
+ return preds;
23
+ }
kit/shell.css ADDED
@@ -0,0 +1,346 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *, *::before, *::after { box-sizing: border-box; }
2
+
3
+ html, body { margin: 0; padding: 0; }
4
+
5
+ body {
6
+ background: var(--bg-canvas);
7
+ color: var(--text-primary);
8
+ font-family: var(--font-sans);
9
+ font-size: var(--text-md);
10
+ line-height: var(--leading-normal);
11
+ -webkit-font-smoothing: antialiased;
12
+ }
13
+
14
+ ::selection { background: var(--selection-bg); color: var(--selection-fg); }
15
+
16
+ a { color: var(--text-accent); text-decoration: none; }
17
+ a:hover { text-decoration: underline; text-underline-offset: 3px; }
18
+
19
+ :focus-visible { outline: none; box-shadow: var(--ring-focus); border-radius: var(--radius-sm); }
20
+
21
+ .page {
22
+ max-width: var(--container-xl);
23
+ margin: 0 auto;
24
+ padding: clamp(1.25rem, 4vw, 4rem);
25
+ }
26
+
27
+ .card {
28
+ padding: clamp(2.5rem, 5vw, 4.5rem) 0;
29
+ border-bottom: var(--border-hair) solid var(--border-subtle);
30
+ }
31
+ .card:last-of-type { border-bottom: 0; }
32
+
33
+ .eyebrow {
34
+ display: flex;
35
+ align-items: baseline;
36
+ gap: var(--space-3);
37
+ font-family: var(--font-mono);
38
+ font-size: var(--text-2xs);
39
+ font-weight: var(--weight-medium);
40
+ letter-spacing: var(--tracking-wider);
41
+ text-transform: uppercase;
42
+ color: var(--text-muted);
43
+ margin-bottom: var(--space-6);
44
+ }
45
+ .eyebrow .tick { color: var(--accent); }
46
+
47
+ .display {
48
+ font-family: var(--font-display);
49
+ font-weight: var(--weight-regular);
50
+ font-size: clamp(3.5rem, 9vw, var(--text-7xl));
51
+ line-height: var(--leading-tight);
52
+ letter-spacing: var(--tracking-tighter);
53
+ margin: 0 0 var(--space-5);
54
+ }
55
+ .display-md {
56
+ font-family: var(--font-display);
57
+ font-weight: var(--weight-regular);
58
+ font-size: clamp(2rem, 4.5vw, var(--text-5xl));
59
+ line-height: var(--leading-tight);
60
+ letter-spacing: var(--tracking-tight);
61
+ margin: 0 0 var(--space-5);
62
+ }
63
+ .display .dot { color: var(--accent); }
64
+
65
+ .lead {
66
+ font-size: clamp(var(--text-lg), 2vw, var(--text-xl));
67
+ line-height: var(--leading-relaxed);
68
+ color: var(--text-secondary);
69
+ max-width: 46em;
70
+ margin: 0;
71
+ }
72
+ .lead .accent { color: var(--text-accent); }
73
+
74
+ /* Research preview banner */
75
+ .preview-banner {
76
+ display: flex;
77
+ flex-wrap: wrap;
78
+ align-items: baseline;
79
+ gap: var(--space-2) var(--space-4);
80
+ margin: 0 0 var(--space-8);
81
+ padding: var(--space-3) var(--space-4);
82
+ border: var(--border-hair) solid var(--border-subtle);
83
+ border-left: 3px solid var(--warning);
84
+ border-radius: var(--radius-md);
85
+ background: var(--bg-surface);
86
+ font-size: var(--text-sm);
87
+ color: var(--text-secondary);
88
+ }
89
+ .preview-banner strong {
90
+ font-family: var(--font-mono);
91
+ font-size: var(--text-2xs);
92
+ font-weight: var(--weight-medium);
93
+ letter-spacing: var(--tracking-wider);
94
+ text-transform: uppercase;
95
+ color: var(--warning);
96
+ }
97
+
98
+ /* Demo panes */
99
+ .demo-grid {
100
+ display: grid;
101
+ grid-template-columns: 1fr 1fr;
102
+ gap: var(--space-8);
103
+ align-items: stretch;
104
+ }
105
+ @media (max-width: 880px) {
106
+ .demo-grid { grid-template-columns: 1fr; }
107
+ }
108
+
109
+ .pane {
110
+ display: flex;
111
+ flex-direction: column;
112
+ gap: var(--space-3);
113
+ min-width: 0;
114
+ }
115
+ .pane-label {
116
+ display: flex;
117
+ align-items: baseline;
118
+ justify-content: space-between;
119
+ gap: var(--space-3);
120
+ font-family: var(--font-mono);
121
+ font-size: var(--text-2xs);
122
+ font-weight: var(--weight-medium);
123
+ letter-spacing: var(--tracking-wider);
124
+ text-transform: uppercase;
125
+ color: var(--text-muted);
126
+ }
127
+
128
+ textarea {
129
+ width: 100%;
130
+ min-height: 280px;
131
+ flex: 1;
132
+ resize: vertical;
133
+ padding: var(--space-4);
134
+ border: var(--border-hair) solid var(--border-default);
135
+ border-radius: var(--radius-lg);
136
+ background: var(--bg-surface);
137
+ box-shadow: var(--shadow-inset);
138
+ color: var(--text-primary);
139
+ font-family: var(--font-mono);
140
+ font-size: var(--text-sm);
141
+ line-height: var(--leading-relaxed);
142
+ }
143
+ textarea:focus-visible { box-shadow: var(--shadow-inset), var(--ring-focus); }
144
+
145
+ .out {
146
+ min-height: 280px;
147
+ flex: 1;
148
+ padding: var(--space-4);
149
+ border: var(--border-hair) solid var(--border-subtle);
150
+ border-radius: var(--radius-lg);
151
+ background: var(--bg-raised);
152
+ font-size: var(--text-md);
153
+ line-height: var(--leading-relaxed);
154
+ white-space: pre-wrap;
155
+ overflow-wrap: break-word;
156
+ overflow-x: auto;
157
+ }
158
+ .out:empty::before {
159
+ content: attr(data-placeholder);
160
+ font-family: var(--font-mono);
161
+ font-size: var(--text-sm);
162
+ color: var(--text-faint);
163
+ }
164
+
165
+ .controls {
166
+ display: flex;
167
+ flex-wrap: wrap;
168
+ align-items: center;
169
+ gap: var(--space-3);
170
+ }
171
+
172
+ .chips {
173
+ display: flex;
174
+ flex-wrap: wrap;
175
+ align-items: center;
176
+ gap: var(--space-2);
177
+ }
178
+ .chips-label {
179
+ font-family: var(--font-mono);
180
+ font-size: var(--text-2xs);
181
+ letter-spacing: var(--tracking-wider);
182
+ text-transform: uppercase;
183
+ color: var(--text-muted);
184
+ }
185
+ .chip {
186
+ padding: 0.4rem 0.85rem;
187
+ border: var(--border-hair) solid var(--border-default);
188
+ border-radius: var(--radius-full);
189
+ background: var(--bg-surface);
190
+ color: var(--text-secondary);
191
+ font-family: var(--font-mono);
192
+ font-size: var(--text-xs);
193
+ letter-spacing: var(--tracking-wide);
194
+ cursor: pointer;
195
+ transition: var(--transition-colors);
196
+ }
197
+ .chip:hover { background: var(--bg-sunken); color: var(--text-primary); }
198
+ .chip[aria-pressed="true"] {
199
+ border-color: var(--accent);
200
+ color: var(--text-accent);
201
+ background: var(--bg-accent-soft);
202
+ }
203
+
204
+ .run-btn {
205
+ height: var(--control-lg);
206
+ padding: 0 var(--space-8);
207
+ border: 0;
208
+ border-radius: var(--radius-md);
209
+ background: var(--accent);
210
+ color: var(--accent-contrast);
211
+ font-family: var(--font-sans);
212
+ font-size: var(--text-base);
213
+ font-weight: var(--weight-semibold);
214
+ letter-spacing: var(--tracking-wide);
215
+ cursor: pointer;
216
+ transition: var(--transition-colors);
217
+ }
218
+ .run-btn:hover { background: var(--accent-hover); }
219
+ .run-btn:disabled { opacity: 0.55; cursor: wait; }
220
+
221
+ .ghost-btn {
222
+ height: 1.75rem;
223
+ padding: 0 var(--space-3);
224
+ border: var(--border-hair) solid var(--border-default);
225
+ border-radius: var(--radius-full);
226
+ background: transparent;
227
+ color: var(--text-muted);
228
+ font-family: var(--font-mono);
229
+ font-size: var(--text-2xs);
230
+ letter-spacing: var(--tracking-wider);
231
+ text-transform: uppercase;
232
+ cursor: pointer;
233
+ transition: var(--transition-colors);
234
+ }
235
+ .ghost-btn:hover { color: var(--text-accent); border-color: var(--accent); }
236
+
237
+ .switch {
238
+ display: inline-flex;
239
+ align-items: center;
240
+ gap: var(--space-2);
241
+ font-family: var(--font-mono);
242
+ font-size: var(--text-2xs);
243
+ letter-spacing: var(--tracking-wider);
244
+ text-transform: uppercase;
245
+ color: var(--text-muted);
246
+ cursor: pointer;
247
+ user-select: none;
248
+ }
249
+ .switch input {
250
+ appearance: none;
251
+ width: 2.1rem;
252
+ height: 1.15rem;
253
+ margin: 0;
254
+ border-radius: var(--radius-full);
255
+ border: var(--border-hair) solid var(--border-default);
256
+ background: var(--bg-sunken);
257
+ position: relative;
258
+ cursor: pointer;
259
+ transition: var(--transition-colors);
260
+ }
261
+ .switch input::after {
262
+ content: "";
263
+ position: absolute;
264
+ top: 1px;
265
+ left: 1px;
266
+ width: 0.95rem;
267
+ height: 0.95rem;
268
+ border-radius: var(--radius-full);
269
+ background: var(--text-muted);
270
+ transition: transform var(--duration-fast) var(--ease-out), background-color var(--duration-fast) var(--ease-standard);
271
+ }
272
+ .switch input:checked { background: var(--accent); border-color: var(--accent); }
273
+ .switch input:checked::after { transform: translateX(0.95rem); background: var(--accent-contrast); }
274
+
275
+ .status {
276
+ display: flex;
277
+ align-items: center;
278
+ gap: var(--space-2);
279
+ min-height: 1.5rem;
280
+ font-family: var(--font-mono);
281
+ font-size: var(--text-xs);
282
+ letter-spacing: var(--tracking-wide);
283
+ color: var(--text-muted);
284
+ }
285
+
286
+ .stats {
287
+ display: grid;
288
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
289
+ gap: var(--space-5);
290
+ margin-top: var(--space-8);
291
+ border-top: var(--border-hair) solid var(--border-subtle);
292
+ padding-top: var(--space-5);
293
+ }
294
+ .stats[hidden] { display: none; }
295
+ .stat { display: flex; flex-direction: column; gap: var(--space-1); }
296
+ .stat .num {
297
+ font-family: var(--font-display);
298
+ font-size: var(--text-2xl);
299
+ line-height: var(--leading-none);
300
+ font-variant-numeric: tabular-nums;
301
+ }
302
+ .stat .label {
303
+ font-family: var(--font-mono);
304
+ font-size: var(--text-2xs);
305
+ letter-spacing: var(--tracking-wide);
306
+ text-transform: uppercase;
307
+ color: var(--text-muted);
308
+ }
309
+
310
+ .table-scroll { overflow-x: auto; }
311
+
312
+ /* Swarm loader (design-system tokens/swarm.css) */
313
+ .swarm { display: none; align-items: center; justify-content: center; }
314
+ .swarm.visible { display: inline-flex; }
315
+ .swarm svg { display: block; overflow: visible; width: 16px; height: 16px; }
316
+ .swarm .dot { fill: var(--text-primary); transform-box: fill-box; transform-origin: center; }
317
+ .swarm .core {
318
+ fill: var(--accent);
319
+ transform-box: fill-box;
320
+ transform-origin: center;
321
+ animation: swarm-core 1.6s ease-in-out infinite;
322
+ }
323
+ @keyframes swarm-core {
324
+ 0%, 100% { transform: scale(0.82); opacity: 0.5; }
325
+ 50% { transform: scale(1.16); opacity: 1; }
326
+ }
327
+ @media (prefers-reduced-motion: reduce) {
328
+ .swarm .dot, .swarm .core {
329
+ animation: none !important;
330
+ opacity: 0.7 !important;
331
+ transform: none !important;
332
+ }
333
+ }
334
+
335
+ .brand-footer {
336
+ display: flex;
337
+ justify-content: space-between;
338
+ gap: var(--space-4);
339
+ padding: var(--space-4) 0;
340
+ border-top: var(--border-hair) solid var(--border-subtle);
341
+ font-family: var(--font-mono);
342
+ font-size: var(--text-2xs);
343
+ letter-spacing: var(--tracking-wider);
344
+ text-transform: uppercase;
345
+ color: var(--text-muted);
346
+ }
kit/tokens.css ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Desert Ant Labs design tokens, mirrored for the shared demo kit.
2
+ *
3
+ * Provenance: desert-ant-labs/design-system tokens/{colors,typography,
4
+ * spacing,radius,motion}.css. Values are copied, not invented; when the
5
+ * brand changes there, re-mirror here and re-sync consuming demos.
6
+ * Two deliberate deviations:
7
+ * - dark mode follows prefers-color-scheme (the design system keys off
8
+ * [data-theme="dark"]; a static page has no theme switch), and
9
+ * - fonts are self-hosted woff2 subsets (the design system loads the
10
+ * same faces from the Google Fonts CDN; this page makes no external
11
+ * requests). Instrument Serif, Hanken Grotesk, and JetBrains Mono are
12
+ * all SIL Open Font License 1.1; see assets/fonts/NOTICE.
13
+ */
14
+
15
+ @font-face {
16
+ font-family: "Hanken Grotesk";
17
+ font-style: normal;
18
+ font-weight: 400 700;
19
+ font-display: swap;
20
+ src: url("assets/fonts/HankenGrotesk-latin-wght.woff2") format("woff2");
21
+ }
22
+ @font-face {
23
+ font-family: "Instrument Serif";
24
+ font-style: normal;
25
+ font-weight: 400;
26
+ font-display: swap;
27
+ src: url("assets/fonts/InstrumentSerif-latin-Regular.woff2") format("woff2");
28
+ }
29
+ @font-face {
30
+ font-family: "Instrument Serif";
31
+ font-style: italic;
32
+ font-weight: 400;
33
+ font-display: swap;
34
+ src: url("assets/fonts/InstrumentSerif-latin-Italic.woff2") format("woff2");
35
+ }
36
+ @font-face {
37
+ font-family: "JetBrains Mono";
38
+ font-style: normal;
39
+ font-weight: 400 500;
40
+ font-display: swap;
41
+ src: url("assets/fonts/JetBrainsMono-latin-wght.woff2") format("woff2");
42
+ }
43
+
44
+ :root {
45
+ /* colors.css · base ramp, cool silver -> obsidian */
46
+ --sand-50: #F4F5F5;
47
+ --sand-100: #E8EAEB;
48
+ --sand-200: #D7DBDC;
49
+ --sand-300: #BEC4C6;
50
+ --sand-400: #9DA4A8;
51
+ --sand-500: #7A8286;
52
+ --sand-600: #5A6266;
53
+ --sand-700: #41484C;
54
+ --sand-800: #2C3134;
55
+ --sand-900: #1A1E20;
56
+ --sand-950: #0E1113;
57
+
58
+ /* colors.css · polarized-sky cobalt accent */
59
+ --sky-50: #ECF0FC;
60
+ --sky-100: #D6DFFA;
61
+ --sky-200: #AFC1F3;
62
+ --sky-300: #839DEB;
63
+ --sky-400: #5675DC;
64
+ --sky-500: #2D52C8;
65
+ --sky-600: #2241A8;
66
+ --sky-700: #1C3585;
67
+
68
+ /* colors.css · signals (amber is the one warm note) */
69
+ --moss-500: #3C8A52;
70
+ --amber-500: #C2691A;
71
+ --oxide-500: #BE3A2E;
72
+
73
+ --paper: #FBFCFC;
74
+ --white: #FFFFFF;
75
+
76
+ /* colors.css · semantic aliases, light */
77
+ --bg-canvas: var(--sand-50);
78
+ --bg-surface: var(--paper);
79
+ --bg-raised: var(--white);
80
+ --bg-sunken: var(--sand-100);
81
+ --bg-accent-soft: var(--sky-50);
82
+
83
+ --text-primary: var(--sand-950);
84
+ --text-secondary: var(--sand-700);
85
+ --text-muted: var(--sand-500);
86
+ --text-faint: var(--sand-400);
87
+ --text-accent: var(--sky-600);
88
+
89
+ --border-subtle: var(--sand-200);
90
+ --border-default: var(--sand-300);
91
+
92
+ --accent: var(--sky-500);
93
+ --accent-hover: var(--sky-600);
94
+ --accent-contrast: var(--white);
95
+ --focus-ring: var(--sky-500);
96
+ --selection-bg: var(--sky-200);
97
+ --selection-fg: var(--sand-950);
98
+ --warning: var(--amber-500);
99
+
100
+ /* typography.css */
101
+ --font-display: "Instrument Serif", "Spectral", Georgia, "Times New Roman", serif;
102
+ --font-sans: "Hanken Grotesk", "Helvetica Neue", Helvetica, Arial, system-ui, sans-serif;
103
+ --font-mono: "JetBrains Mono", "SFMono-Regular", ui-monospace, "Liberation Mono", Menlo, monospace;
104
+
105
+ --weight-regular: 400;
106
+ --weight-medium: 500;
107
+ --weight-semibold: 600;
108
+ --weight-bold: 700;
109
+
110
+ --text-2xs: 0.6875rem;
111
+ --text-xs: 0.75rem;
112
+ --text-sm: 0.8125rem;
113
+ --text-base: 0.9375rem;
114
+ --text-md: 1rem;
115
+ --text-lg: 1.125rem;
116
+ --text-xl: 1.375rem;
117
+ --text-2xl: 1.75rem;
118
+ --text-3xl: 2.25rem;
119
+ --text-5xl: 4rem;
120
+ --text-7xl: 7.5rem;
121
+
122
+ --leading-none: 1;
123
+ --leading-tight: 1.06;
124
+ --leading-snug: 1.2;
125
+ --leading-normal: 1.45;
126
+ --leading-relaxed: 1.62;
127
+
128
+ --tracking-tighter: -0.03em;
129
+ --tracking-tight: -0.015em;
130
+ --tracking-wide: 0.04em;
131
+ --tracking-wider: 0.1em;
132
+
133
+ /* spacing.css */
134
+ --space-1: 0.25rem;
135
+ --space-2: 0.5rem;
136
+ --space-3: 0.75rem;
137
+ --space-4: 1rem;
138
+ --space-5: 1.25rem;
139
+ --space-6: 1.5rem;
140
+ --space-8: 2rem;
141
+ --space-10: 2.5rem;
142
+ --space-12: 3rem;
143
+ --space-16: 4rem;
144
+ --container-xl: 1280px;
145
+ --control-md: 2.5rem;
146
+ --control-lg: 3rem;
147
+
148
+ /* radius.css */
149
+ --radius-sm: 4px;
150
+ --radius-md: 6px;
151
+ --radius-lg: 8px;
152
+ --radius-full: 9999px;
153
+ --border-hair: 1px;
154
+ --shadow-inset: inset 0 1px 2px rgba(40, 30, 15, 0.07);
155
+ --ring-focus: 0 0 0 2px var(--bg-surface), 0 0 0 4px var(--focus-ring);
156
+
157
+ /* motion.css */
158
+ --duration-fast: 140ms;
159
+ --duration-base: 220ms;
160
+ --ease-standard: cubic-bezier(0.2, 0, 0, 1);
161
+ --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
162
+ --transition-colors: color var(--duration-fast) var(--ease-standard),
163
+ background-color var(--duration-fast) var(--ease-standard),
164
+ border-color var(--duration-fast) var(--ease-standard);
165
+
166
+ color-scheme: light dark;
167
+ }
168
+
169
+ @media (prefers-color-scheme: dark) {
170
+ :root {
171
+ /* colors.css · semantic aliases, dark (cool obsidian, lifted cobalt) */
172
+ --bg-canvas: #0E1113;
173
+ --bg-surface: #15181B;
174
+ --bg-raised: #1C2023;
175
+ --bg-sunken: #090B0C;
176
+ --bg-accent-soft: #15203A;
177
+
178
+ --text-primary: var(--sand-50);
179
+ --text-secondary: var(--sand-300);
180
+ --text-muted: var(--sand-500);
181
+ --text-faint: var(--sand-600);
182
+ --text-accent: var(--sky-300);
183
+
184
+ --border-subtle: #262C30;
185
+ --border-default: #353C40;
186
+
187
+ --accent: var(--sky-400);
188
+ --accent-hover: var(--sky-300);
189
+ --accent-contrast: var(--sand-950);
190
+ --focus-ring: var(--sky-300);
191
+ --selection-bg: var(--sky-700);
192
+ --selection-fg: var(--sand-50);
193
+ --warning: #D88B3A;
194
+ }
195
+ }
196
+
197
+ @media (prefers-reduced-motion: reduce) {
198
+ :root {
199
+ --duration-fast: 0ms;
200
+ --duration-base: 0ms;
201
+ }
202
+ }
models/meta.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"labels": ["en", "es", "pt", "fr", "de", "it", "nl", "pl", "tr", "sv", "da", "no", "fi", "cs", "ro", "hu", "id", "vi", "ca", "hr", "sk", "lt", "et", "af", "eu", "gl", "is", "tl", "ms", "az", "sl", "sw", "ga", "sq", "cy", "lv", "eo", "la", "ku", "yo", "xh", "st", "ru", "uk", "bg", "sr", "mk", "be", "kk", "ky", "ar", "fa", "ug", "ur", "hi", "mr", "ne", "bn", "as"], "num_buckets": 65536, "dim": 32, "ngram_orders": [1, 2, 3, 4, 5], "embed_scale": 0.06380146507203109, "latin_labels": ["en", "es", "pt", "fr", "de", "it", "nl", "pl", "tr", "sv", "da", "no", "fi", "cs", "ro", "hu", "id", "vi", "ca", "hr", "sk", "lt", "et", "af", "eu", "gl", "is", "tl", "ms", "az", "sl", "sw", "ga", "sq", "cy", "lv", "eo", "la", "ku", "yo", "xh", "st"], "layout": {"emb_int8": [65536, 32], "lin_w_f32": [59, 32], "lin_b_f32": [59]}}
models/model.b64 ADDED
The diff for this file is too large to render. See raw diff
 
models/names.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"en": ["English", "English"], "es": ["Spanish", "Español"], "pt": ["Portuguese", "Português"], "fr": ["French", "Français"], "de": ["German", "Deutsch"], "it": ["Italian", "Italiano"], "nl": ["Dutch", "Nederlands"], "pl": ["Polish", "Polski"], "tr": ["Turkish", "Türkçe"], "sv": ["Swedish", "Svenska"], "da": ["Danish", "Dansk"], "no": ["Norwegian", "Norsk"], "fi": ["Finnish", "Suomi"], "cs": ["Czech", "Čeština"], "ro": ["Romanian", "Română"], "hu": ["Hungarian", "Magyar"], "id": ["Indonesian", "Indonesia"], "vi": ["Vietnamese", "Tiếng Việt"], "ca": ["Catalan", "Català"], "hr": ["Croatian", "Hrvatski"], "sk": ["Slovak", "Slovenčina"], "lt": ["Lithuanian", "Lietuvių"], "et": ["Estonian", "Eesti"], "af": ["Afrikaans", "Afrikaans"], "eu": ["Basque", "Euskara"], "gl": ["Galician", "Galego"], "is": ["Icelandic", "Íslenska"], "tl": ["Tagalog", "Tagalog"], "ms": ["Malay", "Melayu"], "az": ["Azerbaijani", "Azərbaycan"], "sl": ["Slovenian", "Slovenščina"], "sw": ["Swahili", "Kiswahili"], "ga": ["Irish", "Gaeilge"], "sq": ["Albanian", "Shqip"], "cy": ["Welsh", "Cymraeg"], "lv": ["Latvian", "Latviešu"], "eo": ["Esperanto", "Esperanto"], "la": ["Latin", "Latina"], "ku": ["Kurdish", "Kurdî"], "yo": ["Yoruba", "Yorùbá"], "xh": ["Xhosa", "isiXhosa"], "st": ["Sotho", "Sesotho"], "ru": ["Russian", "Русский"], "uk": ["Ukrainian", "Українська"], "bg": ["Bulgarian", "Български"], "sr": ["Serbian", "Српски"], "mk": ["Macedonian", "Македонски"], "be": ["Belarusian", "Беларуская"], "kk": ["Kazakh", "Қазақ"], "ky": ["Kyrgyz", "Кыргыз"], "ar": ["Arabic", "العربية"], "fa": ["Persian", "فارسی"], "ug": ["Uyghur", "ئۇيغۇر"], "ur": ["Urdu", "اردو"], "hi": ["Hindi", "हिन्दी"], "mr": ["Marathi", "मराठी"], "ne": ["Nepali", "नेपाली"], "bn": ["Bengali", "বাংলা"], "as": ["Assamese", "অসমীয়া"], "ko": ["Korean", "한국어"], "ja": ["Japanese", "日本語"], "zh": ["Chinese", "中文"], "th": ["Thai", "ไทย"], "lo": ["Lao", "ລາວ"], "km": ["Khmer", "ខ្មែរ"], "my": ["Burmese", "မြန်မာ"], "el": ["Greek", "Ελληνικά"], "he": ["Hebrew", "עברית"], "hy": ["Armenian", "Հայերեն"], "ka": ["Georgian", "ქართული"], "am": ["Amharic", "አማርኛ"], "ta": ["Tamil", "தமிழ்"], "te": ["Telugu", "తెలుగు"], "kn": ["Kannada", "ಕನ್ನಡ"], "ml": ["Malayalam", "മലയാളം"], "si": ["Sinhala", "සිංහල"], "gu": ["Gujarati", "ગુજરાતી"], "pa": ["Punjabi", "ਪੰਜਾਬੀ"], "or": ["Odia", "ଓଡ଼ିଆ"], "dv": ["Dhivehi", "ދިވެހި"], "bo": ["Tibetan", "བོད"], "mn": ["Mongolian", "Монгол"], "chr": ["Cherokee", "ᏣᎳᎩ"], "syr": ["Syriac", "ܣܘܪܝܝܐ"], "und": ["Unknown", "—"]}
tongue.css ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Model-specific result styling for the tongue demo. Uses kit tokens only;
2
+ the shared kit under kit/ is never hand-edited. */
3
+ #raw-text {
4
+ font-family: var(--font-display);
5
+ font-size: clamp(1.4rem, 3.5vw, 2rem);
6
+ line-height: var(--leading-snug);
7
+ }
8
+ .lang-note {
9
+ font-size: var(--text-sm);
10
+ color: var(--warning);
11
+ margin: 0 0 var(--space-4);
12
+ }
13
+ .lang-head {
14
+ display: flex;
15
+ align-items: baseline;
16
+ gap: var(--space-3);
17
+ flex-wrap: wrap;
18
+ margin-bottom: var(--space-5);
19
+ min-width: 0;
20
+ }
21
+ .lang-primary {
22
+ font-family: var(--font-display);
23
+ font-size: clamp(2.2rem, 5vw, 3rem);
24
+ line-height: 1;
25
+ color: var(--accent);
26
+ }
27
+ .lang-primary.soft { color: var(--text-secondary); font-size: clamp(1.7rem, 4vw, 2.2rem); }
28
+ .lang-native { font-size: var(--text-lg); color: var(--text-muted); }
29
+ .lang-bars { display: flex; flex-direction: column; gap: var(--space-2); }
30
+ .lang-row {
31
+ display: grid;
32
+ grid-template-columns: 7.5rem 1fr 3.4rem;
33
+ align-items: center;
34
+ gap: var(--space-3);
35
+ font-size: var(--text-base);
36
+ }
37
+ .lang-name { color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
38
+ .lang-bar { height: .5rem; background: var(--bg-sunken); border-radius: var(--radius-full); overflow: hidden; }
39
+ .lang-fill {
40
+ height: 100%;
41
+ background: var(--accent);
42
+ border-radius: var(--radius-full);
43
+ transform-origin: left;
44
+ transition: transform var(--duration-base) var(--ease-out);
45
+ }
46
+ .lang-fill.alt { background: var(--border-default); }
47
+ .lang-pct { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--text-muted); text-align: right; font-variant-numeric: tabular-nums; }
48
+ @media (prefers-reduced-motion: reduce) { .lang-fill { transition: none; } }