Spaces:
Running
Running
<html> | |
<head> | |
<meta content="text/htmlcharset=utf-8" http-equiv="Content-Type" /> | |
<meta name="apple-mobile-web-app-capable" content="yes"> | |
<link rel="shortcut icon" href="candle.png"> | |
<link rel="apple-touch-icon" href="candle.png"> | |
<title>Candle Phi Rust/WASM</title> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<link rel="stylesheet" | |
href="https://cdn.jsdelivr.net/gh/highlightjs/[email protected]/build/styles/default.min.css" /> | |
<style> | |
@import url("https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@200300400&family=Source+Sans+3:wght@100200300400500600700800900&display=swap"); | |
html, | |
body { | |
font-family: "Source Sans 3", sans-serif; | |
} | |
code, | |
output, | |
select, | |
pre { | |
font-family: "Source Code Pro", monospace; | |
} | |
</style> | |
<style type="text/tailwindcss"> | |
.link { @apply underline hover:text-blue-500 hover:no-underline; } | |
</style> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<script type="module"> | |
import snarkdown from "https://cdn.skypack.dev/snarkdown" | |
import hljs from "https://cdn.skypack.dev/highlight.js" | |
// models base url | |
const MODELS = { | |
phi_1_5_q4k: { | |
base_url: | |
"https://huggingface.co/lmz/candle-quantized-phi/resolve/main/", | |
model: "model-q4k.gguf", | |
tokenizer: "tokenizer.json", | |
config: "phi-1_5.json", | |
quantized: true, | |
seq_len: 2048, | |
size: "800 MB", | |
}, | |
phi_1_5_q80: { | |
base_url: | |
"https://huggingface.co/lmz/candle-quantized-phi/resolve/main/", | |
model: "model-q80.gguf", | |
tokenizer: "tokenizer.json", | |
config: "phi-1_5.json", | |
quantized: true, | |
seq_len: 2048, | |
size: "1.51 GB", | |
}, | |
phi_2_0_q4k: { | |
base_url: | |
"https://huggingface.co/radames/phi-2-quantized/resolve/main/", | |
model: [ | |
"model-v2-q4k.gguf_aa.part", | |
"model-v2-q4k.gguf_ab.part", | |
"model-v2-q4k.gguf_ac.part", | |
], | |
tokenizer: "tokenizer.json", | |
config: "config.json", | |
quantized: true, | |
seq_len: 2048, | |
size: "1.57GB", | |
}, | |
puffin_phi_v2_q4k: { | |
base_url: | |
"https://huggingface.co/lmz/candle-quantized-phi/resolve/main/", | |
model: "model-puffin-phi-v2-q4k.gguf", | |
tokenizer: "tokenizer-puffin-phi-v2.json", | |
config: "puffin-phi-v2.json", | |
quantized: true, | |
seq_len: 2048, | |
size: "798 MB", | |
}, | |
puffin_phi_v2_q80: { | |
base_url: | |
"https://huggingface.co/lmz/candle-quantized-phi/resolve/main/", | |
model: "model-puffin-phi-v2-q80.gguf", | |
tokenizer: "tokenizer-puffin-phi-v2.json", | |
config: "puffin-phi-v2.json", | |
quantized: true, | |
seq_len: 2048, | |
size: "1.50 GB", | |
}, | |
} | |
const phiWorker = new Worker("./phiWorker.js", { | |
type: "module", | |
}) | |
async function generateSequence(prompt, controller) { | |
const getValue = (id) => document.querySelector(`#${id}`).value | |
const modelID = getValue("model") | |
const model = MODELS[modelID] | |
const weightsURL = | |
model.model instanceof Array | |
? model.model.map((m) => model.base_url + m) | |
: model.base_url + model.model | |
const tokenizerURL = model.base_url + model.tokenizer | |
const configURL = model.base_url + model.config | |
// const prompt = getValue("prompt").trim() | |
const temperature = getValue("temperature") | |
const topP = getValue("top-p") | |
const repeatPenalty = getValue("repeat_penalty") | |
const seed = getValue("seed") | |
const maxSeqLen = getValue("max-seq") | |
function updateStatus(data) { | |
const outStatus = document.querySelector("#output-status") | |
const outGen = document.querySelector("#output-generation") | |
const outCounter = document.querySelector("#output-counter") | |
switch (data.status) { | |
case "loading": | |
outStatus.hidden = false | |
outStatus.innerHTML = data.message.replaceAll("\n", "<br>\n") | |
outGen.hidden = true | |
outCounter.hidden = true | |
break | |
case "generating": | |
const { message, prompt, sentence, tokensSec, totalTime } = data | |
outStatus.hidden = true | |
outCounter.hidden = false | |
outGen.hidden = false | |
outGen.innerHTML = snarkdown(prompt + sentence).replaceAll('\n', '<br>\n') | |
outCounter.innerHTML = `${(totalTime / 1000).toFixed( | |
2 | |
)}s (${tokensSec.toFixed(2)} tok/s)` | |
hljs.highlightAll() | |
break | |
case "complete": | |
outStatus.hidden = true | |
outGen.hidden = false | |
break | |
} | |
} | |
function decodeHtml(html) { | |
var txt = document.createElement('textarea') | |
txt.innerHTML = html | |
return txt.value | |
} | |
return new Promise((resolve, reject) => { | |
let TEXT = document.querySelector('textarea#terminate')?.innerText | |
if (TEXT === '') TEXT = decodeHtml(document.querySelector('textarea#terminate')?.innerHTML) | |
phiWorker.postMessage({ | |
weightsURL, | |
modelID, | |
tokenizerURL, | |
configURL, | |
quantized: model.quantized, | |
prompt, | |
temp: temperature, | |
top_p: topP, | |
repeatPenalty, | |
seed: seed, | |
maxSeqLen, | |
command: "start", | |
stuff: TEXT.split(',').map(e => e.trim()) | |
}) | |
const handleAbort = () => { | |
phiWorker.postMessage({ command: "abort" }) | |
} | |
const handleMessage = (event) => { | |
const { status, error, message, prompt, sentence } = event.data | |
if (status) updateStatus(event.data) | |
if (error) { | |
phiWorker.removeEventListener("message", handleMessage) | |
reject(new Error(error)) | |
} | |
if (status === "aborted") { | |
phiWorker.removeEventListener("message", handleMessage) | |
resolve(event.data) | |
} | |
if (status === "complete") { | |
phiWorker.removeEventListener("message", handleMessage) | |
resolve(event.data) | |
} | |
} | |
controller.signal.addEventListener("abort", handleAbort) | |
phiWorker.addEventListener("message", handleMessage) | |
}) | |
} | |
const form = document.querySelector("#form") | |
const prompt = document.querySelector("#prompt") | |
const clearBtn = document.querySelector("#clear-btn") | |
const runBtn = document.querySelector("#run") | |
const modelSelect = document.querySelector("#model") | |
let runController = new AbortController() | |
let isRunning = false | |
document.addEventListener("DOMContentLoaded", () => { | |
for (const [id, model] of Object.entries(MODELS)) { | |
const option = document.createElement("option") | |
option.value = id | |
option.innerText = `${id} (${model.size})` | |
modelSelect.appendChild(option) | |
} | |
const query = new URLSearchParams(window.location.search) | |
const modelID = query.get("model") | |
if (modelID) { | |
modelSelect.value = modelID | |
} else { | |
modelSelect.value = "phi_1_5_q4k" | |
} | |
}) | |
const TEMPLATES = { entries: () => [] } | |
for (const [i, { title, prompt }] of TEMPLATES.entries()) { | |
const div = document.createElement("div") | |
const input = document.createElement("input") | |
input.type = "radio" | |
input.name = "task" | |
input.id = `templates-${i}` | |
input.classList.add("font-light", "cursor-pointer") | |
input.value = prompt | |
const label = document.createElement("label") | |
label.htmlFor = `templates-${i}` | |
label.classList.add("cursor-pointer") | |
label.innerText = title | |
div.appendChild(input) | |
div.appendChild(label) | |
promptTemplates.appendChild(div) | |
} | |
modelSelect.addEventListener("change", (e) => { | |
const query = new URLSearchParams(window.location.search) | |
query.set("model", e.target.value) | |
window.history.replaceState( | |
{}, | |
"", | |
`${window.location.pathname}?${query}` | |
) | |
window.parent.postMessage({ queryString: "?" + query }, "*") | |
const model = MODELS[e.target.value] | |
document.querySelector("#max-seq").max = model.seq_len | |
document.querySelector("#max-seq").nextElementSibling.value = 200 | |
}) | |
form.addEventListener("submit", async (e) => { | |
e.preventDefault() | |
if (isRunning) { | |
stopRunning() | |
} else { | |
startRunning() | |
await generateSequence(document.querySelector(`#prompt`).value, runController) | |
stopRunning() | |
} | |
}) | |
function startRunning() { | |
isRunning = true | |
runBtn.textContent = "Stop" | |
} | |
function stopRunning() { | |
runController.abort() | |
runController = new AbortController() | |
runBtn.textContent = "Run" | |
isRunning = false | |
} | |
clearBtn.addEventListener("click", (e) => { | |
e.preventDefault() | |
prompt.value = "" | |
clearBtn.classList.add("invisible") | |
runBtn.disabled = true | |
stopRunning() | |
}) | |
prompt.addEventListener("input", (e) => { | |
runBtn.disabled = false | |
if (e.target.value.length > 0) { | |
clearBtn.classList.remove("invisible") | |
} else { | |
clearBtn.classList.add("invisible") | |
} | |
}) | |
</script> | |
</head> | |
<body class="container max-w-4xl mx-auto p-4 text-gray-800"> | |
<main class="grid grid-cols-1 gap-8 relative"> | |
<span class="absolute text-5xl -ml-[1em]"> 🕯️ </span> | |
<div> | |
<h1 class="text-5xl font-bold">Candle Phi 1.5 / Phi 2.0</h1> | |
</div> | |
<div> | |
<p class="text-m max-w-lg"> | |
<b>Note:</b> | |
When first run, the app will download and cache the model, which could | |
take a few minutes. The models are <b>~800MB</b> or <b>~1.57GB</b> in | |
size. | |
</p> | |
</div> | |
<div> | |
<label for="model" class="font-medium">Models Options: </label> | |
<select id="model" class="border-2 border-gray-500 rounded-md font-light"></select> | |
</div> | |
<form id="form" class="flex text-normal px-1 py-1 border border-gray-700 rounded-md items-center"> | |
<input type="submit" hidden /> | |
<textarea type="text" id="prompt" class="font-light text-lg w-full px-3 py-2 mx-1 resize-none outline-none" | |
oninput="this.style.height = 0; this.style.height = this.scrollHeight + 'px'" | |
placeholder="Add your prompt here..."></textarea> | |
<button id="clear-btn"> | |
<svg fill="none" xmlns="http://www.w3.org/2000/svg" width="40" viewBox="0 0 70 40"> | |
<path opacity=".5" d="M39 .2v40.2" stroke="#1F2937" /> | |
<path d="M1.5 11.5 19 29.1m0-17.6L1.5 29.1" opacity=".5" stroke="#1F2937" stroke-width="2" /> | |
</svg> | |
</button> | |
<button id="run" | |
class="bg-gray-700 hover:bg-gray-800 text-white font-normal py-2 w-16 rounded disabled:bg-gray-300 disabled:cursor-not-allowed"> | |
Run | |
</button> | |
</form> | |
<details> | |
<summary class="font-medium cursor-pointer">Advanced Options</summary> | |
<div class="grid grid-cols-3 max-w-md items-center gap-3 py-3"> | |
<label class="text-sm font-medium" for="max-seq">Maximum length | |
</label> | |
<input type="range" id="max-seq" name="max-seq" min="1" max="2048" step="1" value="200" | |
oninput="this.nextElementSibling.value = Number(this.value)" /> | |
<output class="text-xs w-[50px] text-center font-light px-1 py-1 border border-gray-700 rounded-md"> | |
200</output> | |
<label class="text-sm font-medium" for="temperature">Temperature</label> | |
<input type="range" id="temperature" name="temperature" min="0" max="2" step="0.01" value="0.50" | |
oninput="this.nextElementSibling.value = Number(this.value).toFixed(2)" /> | |
<output class="text-xs w-[50px] text-center font-light px-1 py-1 border border-gray-700 rounded-md"> | |
0.50</output> | |
<label class="text-sm font-medium" for="top-p">Top-p</label> | |
<input type="range" id="top-p" name="top-p" min="0" max="1" step="0.01" value="1.00" | |
oninput="this.nextElementSibling.value = Number(this.value).toFixed(2)" /> | |
<output class="text-xs w-[50px] text-center font-light px-1 py-1 border border-gray-700 rounded-md"> | |
1.00</output> | |
<label class="text-sm font-medium" for="repeat_penalty">Repeat Penalty</label> | |
<input type="range" id="repeat_penalty" name="repeat_penalty" min="1" max="2" step="0.01" value="1.10" | |
oninput="this.nextElementSibling.value = Number(this.value).toFixed(2)" /> | |
<output | |
class="text-xs w-[50px] text-center font-light px-1 py-1 border border-gray-700 rounded-md">1.10</output> | |
<label class="text-sm font-medium" for="seed">Seed</label> | |
<input type="number" id="seed" name="seed" value="299792458" | |
class="font-light border border-gray-700 text-right rounded-md p-2" /> | |
<button id="run" | |
onclick="document.querySelector('#seed').value = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)" | |
class="bg-gray-700 hover:bg-gray-800 text-white font-normal py-1 w-[50px] rounded disabled:bg-gray-300 disabled:cursor-not-allowed text-sm"> | |
Rand | |
</button> | |
<label hidden class="text-sm font-medium" for="terminate">End tokens</label> | |
<textarea hidden type="text" id="terminate" | |
class="font-light text-lg w-full px-3 py-2 mx-1 resize-none outline-none" | |
style="padding-left: -10px; border: 1px solid black; border-radius: 5px; width: 500px" | |
oninput="this.style.height = 0; this.style.height = this.scrollHeight + 'px'" | |
placeholder="Add your terminate tokens here, Separated by `, `"><|endoftext|>, <|user|>, <|system|>, <|assistant|></textarea> | |
</div> | |
</details> | |
<div> | |
<h3 class="font-medium">Generation:</h3> | |
<div class="min-h-[250px] bg-slate-100 text-gray-500 p-4 rounded-md flex flex-col gap-2"> | |
<div id="output-counter" hidden class="ml-auto font-semibold grid-rows-1"></div> | |
<p hidden id="output-generation" class="grid-rows-2 text-lg"></p> | |
<span id="output-status" class="m-auto font-light">No output yet</span> | |
</div> | |
</div> | |
</main> | |
</body> | |
</html> |