| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <title>Stheno v3.4 — WebGPU</title> |
| <style> |
| body { font-family: -apple-system, sans-serif; background: #0a0e14; color: #c9d1d9; max-width: 800px; margin: 0 auto; padding: 20px; } |
| h1 { color: #58a6ff; font-size: 20px; } |
| .card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; margin: 12px 0; } |
| button { background: #238636; color: white; border: none; border-radius: 6px; padding: 10px 20px; cursor: pointer; font-weight: bold; font-size: 14px; margin: 4px; } |
| button:disabled { opacity: 0.4; } |
| #status { color: #e8c87a; font-size: 13px; margin: 8px 0; } |
| #chat { background: #0d1117; border: 1px solid #30363d; border-radius: 8px; padding: 16px; margin: 12px 0; min-height: 300px; max-height: 500px; overflow-y: auto; } |
| .msg { margin: 8px 0; padding: 8px 12px; border-radius: 6px; white-space: pre-wrap; line-height: 1.5; } |
| .user { background: #1f3a5f; color: #e0e8f0; } |
| .assistant { background: #1a2332; color: #c9d1d9; } |
| #input-row { display: flex; gap: 8px; margin-top: 8px; } |
| #input { flex: 1; background: #0d1117; color: #c9d1d9; border: 1px solid #30363d; border-radius: 6px; padding: 10px; font-size: 14px; resize: none; } |
| .info { color: #8b949e; font-size: 12px; } |
| </style> |
| </head> |
| <body> |
| <h1>Stheno v3.4 (Llama-3.1-8B) on WebGPU</h1> |
| <p>Sao10K's character voice model. 4.6 GB Q4_K_M. The voice of the Garden entities.</p> |
|
|
| <div class="card"> |
| <button id="btn-load" onclick="doLoad()">Load Model (4.6 GB)</button> |
| <div id="status">Click Load to start</div> |
| </div> |
|
|
| <div id="chat"></div> |
| <div id="input-row"> |
| <textarea id="input" rows="2" placeholder="Talk..." disabled></textarea> |
| <button id="btn-send" onclick="doSend()" disabled>Send</button> |
| </div> |
| <p class="info">Stheno v3.4 via wllama WebGPU. Upgrade from v3.2. Built for AMD Strix Halo unified memory.</p> |
|
|
| <script type="module"> |
| import { Wllama } from './node_modules/@wllama/wllama/esm/index.js'; |
| |
| let wllama = null; |
| const statusEl = document.getElementById('status'); |
| const chatEl = document.getElementById('chat'); |
| const inputEl = document.getElementById('input'); |
| let history = []; |
| |
| function addMsg(role, text) { |
| const div = document.createElement('div'); |
| div.className = `msg ${role}`; |
| div.textContent = text || ''; |
| chatEl.appendChild(div); |
| chatEl.scrollTop = chatEl.scrollHeight; |
| } |
| |
| window.doLoad = async function() { |
| document.getElementById('btn-load').disabled = true; |
| statusEl.textContent = 'Loading Stheno v3.4...'; |
| wllama = new Wllama( |
| { default: './node_modules/@wllama/wllama/esm/wasm/wllama.wasm' }, |
| { parallelDownloads: 5, logger: { debug:()=>{}, log:m=>statusEl.textContent=m, warn:m=>console.warn(m), error:m=>console.error(m) }} |
| ); |
| await wllama.loadModelFromUrl(window.location.origin + '/model/Stheno-v3.4-Q4_K_M-00001-of-00005.gguf', { |
| n_gpu_layers: 99, n_ctx: 4096, n_batch: 64, useCache: true, |
| progressCallback: ({loaded,total}) => { const p=Math.round(loaded/total*100); if(p%5===0) statusEl.textContent=`Downloading... ${p}%`; }, |
| }); |
| statusEl.textContent = 'Ready — Stheno v3.4 on WebGPU'; |
| inputEl.disabled = false; |
| document.getElementById('btn-send').disabled = false; |
| inputEl.focus(); |
| }; |
| |
| function buildPrompt() { |
| let prompt = '<|begin_of_text|>'; |
| for (const msg of history) { |
| prompt += `<|start_header_id|>${msg.role}<|end_header_id|>\n\n${msg.content}<|eot_id|>`; |
| } |
| prompt += '<|start_header_id|>assistant<|end_header_id|>\n\n'; |
| return prompt; |
| } |
| |
| window.doSend = async function() { |
| const text = inputEl.value.trim(); |
| if (!text || !wllama) return; |
| inputEl.value = ''; |
| inputEl.disabled = true; |
| document.getElementById('btn-send').disabled = true; |
| history.push({ role: 'user', content: text }); |
| addMsg('user', text); |
| const genStart = performance.now(); |
| statusEl.textContent = 'Generating...'; |
| const result = await wllama.createCompletion({ |
| prompt: buildPrompt(), max_tokens: 1024, temperature: 0.8, top_k: 50, repeat_penalty: 1.15, |
| stop: ['<|eot_id|>', '<|start_header_id|>'], |
| }); |
| const rawText = result?.choices?.[0]?.text || result?.text || ''; |
| const cleanText = rawText.replace(/<\|eot_id\|>/g,'').replace(/<\|start_header_id\|>/g,'').trim(); |
| addMsg('assistant', cleanText); |
| history.push({ role: 'assistant', content: cleanText }); |
| statusEl.textContent = `Done — ${((performance.now()-genStart)/1000).toFixed(1)}s`; |
| inputEl.disabled = false; |
| document.getElementById('btn-send').disabled = false; |
| inputEl.focus(); |
| }; |
| |
| inputEl.addEventListener('keydown', e => { if (e.key==='Enter' && !e.shiftKey) { e.preventDefault(); doSend(); } }); |
| </script> |
| </body> |
| </html> |
|
|