Spaces:
Running
Running
Download cloud.js from Fritter-ASR/Fritter-Demo: direct link, hf CLI and curl.
- Browser
- Download file 8.86 kB
-
https://huggingface.co/spaces/Fritter-ASR/Fritter-Demo/resolve/main/cloud.js
- Command line
-
hf download hf://spaces/Fritter-ASR/Fritter-Demo/cloud.js
-
curl -L -o cloud.js https://huggingface.co/spaces/Fritter-ASR/Fritter-Demo/resolve/main/cloud.js
8.86 kB
| // Vocabulary cloud (2026-11-08), shared by danielrosehill/Fritter-Demo and Hebrish/Sabich-Demo (same file in both folders; keep them identical). | |
| // training takes drift left and right, sized by how many takes contain them and coloured by group. Hover or tap a | |
| // word to see a sentence it was recorded in. Data: terms.json, built by ASR-Training scripts/fritter_terms.py from | |
| // danielrosehill/Tech-Sentences meta/*.jsonl. The same file is copied to ai.danielrosehill.com (danielrosehilldotcom). | |
| // | |
| // import { mountCloud } from './cloud.js'; | |
| // mountCloud(document.getElementById('cloud'), await (await fetch('terms.json')).json()); | |
| // | |
| // Needs in the host page: an element to mount into. Styles are injected once (class prefix "fc-"). | |
| const GROUP_COLOURS = { | |
| 'AI & models': '#7c5cff', | |
| 'Speech & audio': '#ff7a45', | |
| 'Dev & data': '#1aa57a', | |
| 'Linux & self-hosting': '#2f8fe6', | |
| 'Smart home': '#e0a100', | |
| 'Hardware': '#e2457a', | |
| }; | |
| const CSS = ` | |
| .fc { position: relative; --fc-ink: currentColor; } | |
| .fc-chips { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 14px; } | |
| .fc-chip { font: inherit; font-size: .82rem; font-weight: 600; border-radius: 999px; padding: 4px 12px; cursor: pointer; | |
| border: 1.5px solid var(--c); background: transparent; color: inherit; transition: background .2s, color .2s; } | |
| .fc-chip[aria-pressed="true"] { background: var(--c); color: #fff; } | |
| .fc-chip .fc-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--c); margin-right: 6px; vertical-align: 1px; } | |
| .fc-chip[aria-pressed="true"] .fc-dot { background: #fff; } | |
| .fc-stage { position: relative; overflow: hidden; border-radius: 18px; padding: 10px 0; | |
| -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); | |
| mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); } | |
| .fc-lane { display: flex; width: max-content; align-items: center; white-space: nowrap; padding: 3px 0; | |
| animation: fc-drift var(--dur) linear infinite; animation-direction: var(--dir); } | |
| .fc-stage:hover .fc-lane.fc-hold, .fc-lane.fc-hold { animation-play-state: paused; } | |
| @keyframes fc-drift { from { transform: translateX(0); } to { transform: translateX(-50%); } } | |
| .fc-w { font-weight: 650; letter-spacing: -.01em; margin: 0 .55em; cursor: pointer; color: var(--c); | |
| transition: opacity .3s, transform .2s, filter .3s; line-height: 1.15; border: 0; background: none; padding: 0; font-family: inherit; } | |
| .fc-w:hover, .fc-w:focus-visible, .fc-w.fc-on { transform: scale(1.18); outline: none; text-shadow: 0 0 18px color-mix(in srgb, var(--c) 55%, transparent); } | |
| .fc.fc-filtered .fc-w:not(.fc-in) { opacity: .12; filter: grayscale(1); } | |
| .fc-card { margin-top: 14px; border-radius: 14px; padding: 14px 16px; min-height: 92px; | |
| background: color-mix(in srgb, var(--c, #888) 10%, transparent); border: 1px solid color-mix(in srgb, var(--c, #888) 35%, transparent); | |
| transition: background .4s, border-color .4s; } | |
| .fc-card .fc-term { font-weight: 700; color: var(--c); font-size: 1.05rem; } | |
| .fc-card .fc-meta { font-size: .8rem; opacity: .7; margin-left: 8px; } | |
| .fc-card .fc-said { margin: 6px 0 0; font-size: 1.05rem; } | |
| .fc-card mark { background: color-mix(in srgb, var(--c) 25%, transparent); color: inherit; border-radius: 4px; padding: 0 3px; } | |
| .fc-foot { font-size: .8rem; opacity: .65; margin-top: 8px; } | |
| @media (prefers-reduced-motion: reduce) { | |
| .fc-lane { animation: none; flex-wrap: wrap; width: auto; white-space: normal; justify-content: center; } | |
| .fc-lane .fc-dup { display: none; } | |
| .fc-stage { -webkit-mask-image: none; mask-image: none; } | |
| }`; | |
| function injectCss() { | |
| if (document.getElementById('fc-css')) return; | |
| const s = document.createElement('style'); | |
| s.id = 'fc-css'; | |
| s.textContent = CSS; | |
| document.head.appendChild(s); | |
| } | |
| const esc = (s) => s.replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); | |
| const reEsc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | |
| function shuffle(a, seed = 7) { | |
| let x = seed; | |
| const r = () => ((x = (x * 9301 + 49297) % 233280) / 233280); | |
| for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(r() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } | |
| return a; | |
| } | |
| export function mountCloud(root, data, opts = {}) { | |
| injectCss(); | |
| const lanes = opts.lanes || (matchMedia('(max-width: 600px)').matches ? 6 : 8); | |
| const colours = { ...GROUP_COLOURS, ...(opts.colours || {}) }; | |
| const terms = data.terms.filter((t) => colours[t.g]); | |
| const max = Math.max(...terms.map((t) => t.n)), min = Math.min(...terms.map((t) => t.n)); | |
| const size = (n) => 0.95 + 1.45 * (Math.log(n) - Math.log(min)) / (Math.log(max) - Math.log(min) || 1); // rem | |
| root.classList.add('fc'); | |
| const groups = [...new Set(terms.map((t) => t.g))].sort((a, b) => Object.keys(colours).indexOf(a) - Object.keys(colours).indexOf(b)); | |
| const counts = Object.fromEntries(groups.map((g) => [g, terms.filter((t) => t.g === g).length])); | |
| root.innerHTML = ` | |
| <div class="fc-chips" role="group" aria-label="Filter by group"> | |
| ${groups.map((g) => `<button type="button" class="fc-chip" style="--c:${colours[g]}" data-g="${esc(g)}" aria-pressed="false"><span class="fc-dot"></span>${esc(g)} <span style="opacity:.7">${counts[g]}</span></button>`).join('')} | |
| </div> | |
| <div class="fc-stage" aria-label="Tech terms Fritter was trained on"></div> | |
| <div class="fc-card" aria-live="polite"></div> | |
| <p class="fc-foot">${terms.length} of the ${data.distinct_terms.toLocaleString()} terms in ${data.rows.toLocaleString()} ${opts.noun || "recorded tech sentences"}, each with 3 or more takes. Bigger means more takes. Hover or tap a word.</p>`; | |
| const stage = root.querySelector('.fc-stage'), card = root.querySelector('.fc-card'); | |
| // Deal terms into lanes so each lane mixes big and small words. | |
| const order = shuffle([...terms]); | |
| const rows = Array.from({ length: lanes }, () => []); | |
| order.forEach((t, i) => rows[i % lanes].push(t)); | |
| const wordHtml = (t, dup) => `<button type="button" class="fc-w${dup ? ' fc-dup' : ''}" style="--c:${colours[t.g]};font-size:${size(t.n).toFixed(2)}rem" data-i="${terms.indexOf(t)}"${dup ? ' tabindex="-1" aria-hidden="true"' : ''}>${esc(t.t)}</button>`; | |
| stage.innerHTML = rows.map((r, i) => `<div class="fc-lane" style="--dur:${(r.length * (5.5 + (i % 3) * 1.6)).toFixed(0)}s;--dir:${i % 2 ? 'reverse' : 'normal'}">${r.map((t) => wordHtml(t, false)).join('')}${r.map((t) => wordHtml(t, true)).join('')}</div>`).join(''); | |
| let active = null, auto = null, filter = null; | |
| function show(t, el) { | |
| card.style.setProperty('--c', colours[t.g]); | |
| const said = t.s ? esc(t.s).replace(new RegExp(`\\b${reEsc(esc(t.t))}\\b`), (m) => `<mark>${m}</mark>`) : ''; | |
| card.innerHTML = `<span class="fc-term">${esc(t.t)}</span><span class="fc-meta">${esc(t.g)} · ${t.n} takes</span>` + | |
| (said ? `<p class="fc-said">“${said}”</p>` : ''); | |
| root.querySelectorAll('.fc-w.fc-on').forEach((w) => w.classList.remove('fc-on')); | |
| if (el) root.querySelectorAll(`.fc-w[data-i="${el.dataset.i}"]`).forEach((w) => w.classList.add('fc-on')); | |
| } | |
| const pool = () => terms.filter((t) => t.s && (!filter || t.g === filter)); | |
| function tick() { const p = pool(); if (!p.length) return; const t = p[Math.floor(Math.random() * p.length)]; show(t, root.querySelector(`.fc-w[data-i="${terms.indexOf(t)}"]`)); } | |
| const startAuto = () => { clearInterval(auto); auto = setInterval(tick, opts.interval || 4200); }; | |
| stage.addEventListener('pointerover', (e) => { | |
| const w = e.target.closest('.fc-w'); if (!w) return; | |
| active = w; w.closest('.fc-lane').classList.add('fc-hold'); clearInterval(auto); show(terms[w.dataset.i], w); | |
| }); | |
| stage.addEventListener('pointerout', (e) => { | |
| const w = e.target.closest('.fc-w'); if (!w) return; | |
| w.closest('.fc-lane').classList.remove('fc-hold'); active = null; startAuto(); | |
| }); | |
| stage.addEventListener('click', (e) => { const w = e.target.closest('.fc-w'); if (w) { clearInterval(auto); show(terms[w.dataset.i], w); setTimeout(startAuto, 6000); } }); | |
| stage.addEventListener('focusin', (e) => { const w = e.target.closest('.fc-w'); if (w) { clearInterval(auto); show(terms[w.dataset.i], w); } }); | |
| root.querySelector('.fc-chips').addEventListener('click', (e) => { | |
| const c = e.target.closest('.fc-chip'); if (!c) return; | |
| filter = filter === c.dataset.g ? null : c.dataset.g; | |
| root.querySelectorAll('.fc-chip').forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.g === filter))); | |
| root.classList.toggle('fc-filtered', !!filter); | |
| root.querySelectorAll('.fc-w').forEach((w) => w.classList.toggle('fc-in', !filter || terms[w.dataset.i].g === filter)); | |
| tick(); startAuto(); | |
| }); | |
| tick(); startAuto(); | |
| return { destroy() { clearInterval(auto); root.innerHTML = ''; } }; | |
| } | |