phi-offline / index.html
plug's picture
Update index.html
ee6d5fb
<!DOCTYPE html>
<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>