Spaces:
Running
Running
standard kit demo: research-preview, hero, try-it, on-device
Browse files- index.html +0 -0
- js/main.js +78 -0
- js/samples.js +14 -0
- js/tongue.js +169 -0
- kit/assets/favicon-32.png +0 -0
- kit/assets/favicon.svg +13 -0
- kit/assets/fonts/HankenGrotesk-latin-wght.woff2 +0 -0
- kit/assets/fonts/InstrumentSerif-latin-Italic.woff2 +0 -0
- kit/assets/fonts/InstrumentSerif-latin-Regular.woff2 +0 -0
- kit/assets/fonts/JetBrainsMono-latin-wght.woff2 +0 -0
- kit/assets/fonts/NOTICE +100 -0
- kit/components/panes.js +83 -0
- kit/components/samples.js +23 -0
- kit/components/shell.js +67 -0
- kit/components/swarm.js +31 -0
- kit/js/tags.js +23 -0
- kit/shell.css +346 -0
- kit/tokens.css +202 -0
- models/meta.json +1 -0
- models/model.b64 +0 -0
- models/names.json +1 -0
- tongue.css +48 -0
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=>({"&":"&","<":"<",">":">"}[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; } }
|