Spaces:
Running
Running
Unify: q-live redirects to canonical Q (text+voice) at HOLOGRAMTECH/q
Browse files- index.html +14 -164
index.html
CHANGED
|
@@ -2,175 +2,25 @@
|
|
| 2 |
<html lang="en">
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8">
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1
|
| 6 |
<title>Q</title>
|
| 7 |
-
<
|
| 8 |
-
|
| 9 |
-
"@huggingface/transformers": "/_shared/voice/vendor/kokoro/transformers/transformers.js",
|
| 10 |
-
"phonemizer": "/_shared/voice/vendor/kokoro/phonemizer.js",
|
| 11 |
-
"fs/promises": "/_shared/voice/vendor/kokoro/stub.js",
|
| 12 |
-
"path": "/_shared/voice/vendor/kokoro/stub.js"
|
| 13 |
-
} }
|
| 14 |
-
</script>
|
| 15 |
<style>
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
.wordmark { position:fixed; top:22px; left:24px; font-size:13px; letter-spacing:.22em; color:var(--dim); font-weight:600; }
|
| 23 |
-
.wordmark b { color:var(--fg); }
|
| 24 |
-
#stage { position:relative; width:min(74vw,340px); height:min(74vw,340px); cursor:pointer; display:grid; place-items:center; }
|
| 25 |
-
canvas { width:100%; height:100%; display:block; }
|
| 26 |
-
.hint { position:absolute; font-size:13px; letter-spacing:.14em; text-transform:uppercase; color:var(--dim);
|
| 27 |
-
pointer-events:none; transition:opacity .4s; }
|
| 28 |
-
.caption { margin-top:40px; max-width:min(88vw,560px); text-align:center; min-height:3.2em; padding:0 8px; }
|
| 29 |
-
.caption .you { color:var(--dim); font-size:16px; }
|
| 30 |
-
.caption .q { color:var(--fg); font-size:21px; line-height:1.45; letter-spacing:-.01em; }
|
| 31 |
-
.caption.fade { opacity:0; transition:opacity 1.2s; }
|
| 32 |
-
.status { margin-top:6px; font-size:12px; letter-spacing:.16em; text-transform:uppercase; color:var(--dim); min-height:16px; }
|
| 33 |
-
.compose { position:fixed; bottom:max(20px,env(safe-area-inset-bottom)); left:50%; transform:translateX(-50%);
|
| 34 |
-
width:min(90vw,520px); display:flex; gap:8px; opacity:.5; transition:opacity .2s; }
|
| 35 |
-
.compose:focus-within { opacity:1; }
|
| 36 |
-
.compose input { flex:1; background:rgba(255,255,255,.04); border:1px solid rgba(255,255,255,.08); color:var(--fg);
|
| 37 |
-
border-radius:14px; padding:11px 15px; font-size:15px; outline:none; }
|
| 38 |
-
.compose input::placeholder { color:#465065; }
|
| 39 |
-
.compose button { background:rgba(255,255,255,.06); border:1px solid rgba(255,255,255,.1); color:var(--fg);
|
| 40 |
-
border-radius:14px; padding:0 16px; font-size:15px; cursor:pointer; }
|
| 41 |
-
.lat { position:fixed; bottom:16px; right:18px; font:11px ui-monospace,Menlo,monospace; color:#333c52; letter-spacing:.04em; text-align:right; }
|
| 42 |
-
.lat b { color:#20d17a; font-weight:600; } /* speculated (fast) turn */
|
| 43 |
-
.stats { position:fixed; bottom:16px; left:18px; font:10.5px/1.5 ui-monospace,Menlo,monospace; color:#333c52; letter-spacing:.02em;
|
| 44 |
-
white-space:pre; opacity:0; transition:opacity .3s; }
|
| 45 |
-
.stats.on { opacity:1; }
|
| 46 |
</style>
|
| 47 |
</head>
|
| 48 |
<body>
|
| 49 |
-
<div class="
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
<
|
| 53 |
-
<div class="hint" id="hint">tap to talk</div>
|
| 54 |
</div>
|
| 55 |
-
|
| 56 |
-
<div class="caption" id="cap"></div>
|
| 57 |
-
<div class="status" id="status"></div>
|
| 58 |
-
|
| 59 |
-
<form class="compose" id="compose" autocomplete="off">
|
| 60 |
-
<input id="text" placeholder="or type to Q" enterkeyhint="send">
|
| 61 |
-
<button type="submit">Say</button>
|
| 62 |
-
</form>
|
| 63 |
-
<div class="lat" id="lat"></div>
|
| 64 |
-
<div class="stats" id="stats"></div>
|
| 65 |
-
|
| 66 |
-
<script>
|
| 67 |
-
if ("serviceWorker" in navigator) { try { navigator.serviceWorker.register("/q-live-sw.js", { scope: "/" }); } catch (e) {} }
|
| 68 |
-
</script>
|
| 69 |
-
<script type="module">
|
| 70 |
-
import { createQLive } from "/apps/q/q-live.mjs";
|
| 71 |
-
|
| 72 |
-
const $ = (id) => document.getElementById(id);
|
| 73 |
-
const q = createQLive({ persona: "You are Q, a private on-device AI that runs entirely in this browser. Answer in one or two short, warm, natural spoken sentences." });
|
| 74 |
-
window.Q = q;
|
| 75 |
-
|
| 76 |
-
let phase = "idle", loading = false, level = 0, capFadeT = 0;
|
| 77 |
-
const setStatus = (s) => $("status").textContent = s;
|
| 78 |
-
const showHint = (on) => $("hint").style.opacity = on ? 1 : 0;
|
| 79 |
-
|
| 80 |
-
function caption(who, text) {
|
| 81 |
-
const el = $("cap"); el.classList.remove("fade");
|
| 82 |
-
el.innerHTML = `<span class="${who}">${text || ""}</span>`;
|
| 83 |
-
capFadeT = performance.now() + 6000; // fade if nothing new for a while
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
q.on("state", (s) => { phase = s;
|
| 87 |
-
showHint(s === "idle" && !q.active && !loading);
|
| 88 |
-
setStatus(loading ? "waking" : s === "idle" ? (q.active ? "" : "") : s);
|
| 89 |
-
});
|
| 90 |
-
q.on("partial", (t) => caption("you", t + " …"));
|
| 91 |
-
q.on("final", (t) => caption("you", t));
|
| 92 |
-
q.on("spoken", (t) => caption("q", t)); // Q's words, revealed IN SYNC with the spoken voice (audio-clock timed)
|
| 93 |
-
// ── per-stage latency ledger: the references' real lesson is that MAGIC IS CONSISTENCY (P95), not a good average.
|
| 94 |
-
// Accumulate first-audio per turn, split COLD vs SPECULATED, and render median + P95 so the win is provable. ──
|
| 95 |
-
const lard = { fa_cold: [], fa_spec: [], ft_cold: [], ft_warm: [] }; // fa = first-audio, ft = first-token (prefill)
|
| 96 |
-
const pct = (a, p) => { if (!a.length) return null; const s = [...a].sort((x, y) => x - y); return s[Math.min(s.length - 1, Math.floor(p / 100 * s.length))]; };
|
| 97 |
-
function renderStats() {
|
| 98 |
-
const row = (name, a) => { if (!a.length) return null; return name.padEnd(11) + " n=" + String(a.length).padEnd(3) + " p50 " + String(pct(a, 50)).padStart(5) + " p95 " + String(pct(a, 95)).padStart(5) + " ms"; };
|
| 99 |
-
const lines = [
|
| 100 |
-
row("audio·spec", lard.fa_spec), row("audio·cold", lard.fa_cold),
|
| 101 |
-
row("1sttok·warm", lard.ft_warm), row("1sttok·cold", lard.ft_cold),
|
| 102 |
-
].filter(Boolean);
|
| 103 |
-
const el = $("stats"); el.textContent = lines.join("\n"); el.classList.toggle("on", lines.length > 0);
|
| 104 |
-
}
|
| 105 |
-
q.on("metrics", (m) => {
|
| 106 |
-
if (m.firstAudio != null && !m.cached) { // live first-audio readout (emitted mid-turn; don't record here)
|
| 107 |
-
const fast = !!m.speculated;
|
| 108 |
-
$("lat").innerHTML = fast ? "<b>spec · " + m.firstAudio + " ms to voice</b>" : m.firstAudio + " ms to voice";
|
| 109 |
-
} else if (m.cached && m.firstAudio != null) { $("lat").innerHTML = "cached · " + m.firstAudio + " ms to voice"; }
|
| 110 |
-
if (m.total == null) return; // record into the ledger ONCE, at turn completion (metrics is emitted repeatedly per turn)
|
| 111 |
-
if (m.cached) return;
|
| 112 |
-
if (m.firstToken != null) (m.warm ? lard.ft_warm : lard.ft_cold).push(m.firstToken); // the warm-KV win lands HERE (prefill)
|
| 113 |
-
if (m.firstAudio != null) (m.speculated ? lard.fa_spec : lard.fa_cold).push(m.firstAudio);
|
| 114 |
-
renderStats();
|
| 115 |
-
});
|
| 116 |
-
q.on("speculating", (t) => console.log("[Q] speculating on:", t));
|
| 117 |
-
q.on("warn", (w) => console.warn("[Q]", w));
|
| 118 |
-
|
| 119 |
-
// ── the orb: one living, audio-reactive presence ──
|
| 120 |
-
const cv = $("orb"), g = cv.getContext("2d"), W = cv.width, H = cv.height, cx = W/2, cy = H/2;
|
| 121 |
-
const PAL = {
|
| 122 |
-
idle: ["#3a5cff", "#6ea8ff"],
|
| 123 |
-
listening: ["#18b6ff", "#7ce0ff"],
|
| 124 |
-
thinking: ["#8a5cff", "#c19bff"],
|
| 125 |
-
speaking: ["#20d17a", "#82f0b4"],
|
| 126 |
-
waking: ["#8a5cff", "#c19bff"],
|
| 127 |
-
};
|
| 128 |
-
function draw(t) {
|
| 129 |
-
const target = q.getLevel ? q.getLevel() : 0;
|
| 130 |
-
level += (target - level) * 0.25; // smooth
|
| 131 |
-
const st = loading ? "waking" : phase;
|
| 132 |
-
const col = PAL[st] || PAL.idle;
|
| 133 |
-
const breathe = 1 + 0.03 * Math.sin(t/900);
|
| 134 |
-
const react = 1 + level * 0.9; // voice-reactive swell
|
| 135 |
-
const R0 = (W * 0.16) * breathe * react;
|
| 136 |
-
g.clearRect(0,0,W,H);
|
| 137 |
-
// soft aura layers
|
| 138 |
-
for (let i = 4; i >= 0; i--) {
|
| 139 |
-
const r = R0 + i * (W*0.05) + (i===0 ? level*40 : 0);
|
| 140 |
-
const grd = g.createRadialGradient(cx, cy, r*0.15, cx, cy, r);
|
| 141 |
-
grd.addColorStop(0, col[1] + (i===0 ? "e6" : "1c"));
|
| 142 |
-
grd.addColorStop(1, col[0] + "00");
|
| 143 |
-
g.beginPath(); g.fillStyle = grd; g.arc(cx, cy, r, 0, 7); g.fill();
|
| 144 |
-
}
|
| 145 |
-
// bright core
|
| 146 |
-
const core = g.createRadialGradient(cx, cy, 0, cx, cy, R0*0.6);
|
| 147 |
-
core.addColorStop(0, "#ffffff" + (st==="idle" ? "cc" : "ee"));
|
| 148 |
-
core.addColorStop(1, col[1] + "00");
|
| 149 |
-
g.beginPath(); g.fillStyle = core; g.arc(cx, cy, R0*0.6, 0, 7); g.fill();
|
| 150 |
-
// caption fade when idle+quiet
|
| 151 |
-
if (capFadeT && performance.now() > capFadeT && phase === "idle") { $("cap").classList.add("fade"); capFadeT = 0; }
|
| 152 |
-
requestAnimationFrame(draw);
|
| 153 |
-
}
|
| 154 |
-
requestAnimationFrame(draw);
|
| 155 |
-
|
| 156 |
-
// ── one gesture: tap the orb to start / end the call ──
|
| 157 |
-
async function toggle() {
|
| 158 |
-
if (loading) return;
|
| 159 |
-
if (q.active) { q.stop(); setStatus(""); showHint(true); return; }
|
| 160 |
-
try { q.arm(); } catch (e) {} // UNLOCK audio inside the gesture (must be synchronous)
|
| 161 |
-
loading = true; setStatus("waking"); showHint(false);
|
| 162 |
-
try { await q.start(); } catch (e) { caption("q", "I couldn't start — " + (e.message || e)); }
|
| 163 |
-
loading = false;
|
| 164 |
-
}
|
| 165 |
-
$("stage").addEventListener("click", toggle);
|
| 166 |
-
|
| 167 |
-
// text drive (no mic) — same brain→voice path
|
| 168 |
-
$("compose").addEventListener("submit", async (e) => {
|
| 169 |
-
e.preventDefault(); const v = $("text").value.trim(); if (!v) return; $("text").value = "";
|
| 170 |
-
try { q.arm(); } catch (e2) {} // unlock audio inside the submit gesture
|
| 171 |
-
caption("you", v);
|
| 172 |
-
try { await q.say(v); } catch (err) { caption("q", "…" + (err.message || err)); }
|
| 173 |
-
});
|
| 174 |
-
</script>
|
| 175 |
</body>
|
| 176 |
</html>
|
|
|
|
| 2 |
<html lang="en">
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
<title>Q</title>
|
| 7 |
+
<meta http-equiv="refresh" content="0; url=https://hologramtech-q.static.hf.space">
|
| 8 |
+
<link rel="canonical" href="https://hologramtech-q.static.hf.space">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
<style>
|
| 10 |
+
html,body{height:100%;margin:0}
|
| 11 |
+
body{background:#0b0f14;color:#e8eef5;font:16px/1.5 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;
|
| 12 |
+
display:flex;align-items:center;justify-content:center;text-align:center}
|
| 13 |
+
a{color:#8ecbff}
|
| 14 |
+
.c{max-width:32ch}
|
| 15 |
+
.d{opacity:.6;font-size:14px;margin-top:.75em}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
</style>
|
| 17 |
</head>
|
| 18 |
<body>
|
| 19 |
+
<div class="c">
|
| 20 |
+
<p>Q now lives in one place — text and voice together.</p>
|
| 21 |
+
<p><a href="https://hologramtech-q.static.hf.space">Open Q →</a></p>
|
| 22 |
+
<p class="d">Redirecting…</p>
|
|
|
|
| 23 |
</div>
|
| 24 |
+
<script>location.replace("https://hologramtech-q.static.hf.space");</script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
</body>
|
| 26 |
</html>
|