|
|
<!doctype html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="utf-8" /> |
|
|
<meta http-equiv="X-UA-Compatible" content="IE=edge"/> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1"/> |
|
|
<title>NLLB Translation (WebGPU/WASM) + Progress</title> |
|
|
<style> |
|
|
body { font: 14px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 16px; } |
|
|
h1 { margin-top:0; } |
|
|
label { display:block; margin: 8px 0 4px; } |
|
|
textarea { width: 100%; min-height: 120px; } |
|
|
select, input[type="submit"] { padding: 8px; font: inherit; } |
|
|
#row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } |
|
|
#progress-wrap { margin: 10px 0; display:none; } |
|
|
#footer { opacity:.7; margin-top:12px; font-size:12px; } |
|
|
.muted { opacity:.8 } |
|
|
.error { color:#b00 } |
|
|
.ok { color:#0a0 } |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<h1>NLLB translation in browser</h1> |
|
|
|
|
|
<div id="row"> |
|
|
<div> |
|
|
<label for="src_lang">Source language</label> |
|
|
<select id="src_lang"> |
|
|
<option value="eng_Latn" selected>English (eng_Latn)</option> |
|
|
<option value="spa_Latn">Spanish (spa_Latn)</option> |
|
|
<option value="fra_Latn">French (fra_Latn)</option> |
|
|
<option value="hin_Deva">Hindi (hin_Deva)</option> |
|
|
</select> |
|
|
</div> |
|
|
<div> |
|
|
<label for="tgt_lang">Target language</label> |
|
|
<select id="tgt_lang"> |
|
|
<option value="spa_Latn" selected>Spanish (spa_Latn)</option> |
|
|
<option value="eng_Latn">English (eng_Latn)</option> |
|
|
<option value="fra_Latn">French (fra_Latn)</option> |
|
|
<option value="hin_Deva">Hindi (hin_Deva)</option> |
|
|
</select> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<label for="from">Input text</label> |
|
|
<textarea id="from" placeholder="Type text to translate"></textarea> |
|
|
|
|
|
|
|
|
<div id="progress-wrap"> |
|
|
<progress id="loadProgress" value="0" max="1" style="width:100%;"></progress> |
|
|
<div id="progressText" class="muted">Initializing…</div> |
|
|
</div> |
|
|
|
|
|
<input type="submit" id="submit" value="Loading..." disabled /> |
|
|
|
|
|
<label for="to" style="margin-top:12px">Output</label> |
|
|
<textarea id="to" readonly>Output will be here...</textarea> |
|
|
|
|
|
<div id="footer" class="muted"> |
|
|
Tip: first load downloads the model; later loads are much faster thanks to browser cache. |
|
|
</div> |
|
|
|
|
|
<div id="libStatus" class="error"></div> |
|
|
|
|
|
|
|
|
<script> |
|
|
(function loadTransformersUMD() { |
|
|
const tries = [ |
|
|
"https://cdn.jsdelivr.net/npm/@xenova/transformers@3.0.0/dist/transformers.min.js", |
|
|
"https://unpkg.com/@xenova/transformers@3.0.0/dist/transformers.min.js", |
|
|
"https://huggingface.co/datasets/Xenova/transformers.js/resolve/main/3.0.0/transformers.min.js", |
|
|
"./transformers.min.js" |
|
|
]; |
|
|
const statusEl = document.getElementById("libStatus"); |
|
|
|
|
|
function tryNext(i) { |
|
|
if (i >= tries.length) { |
|
|
statusEl.textContent = "❌ Failed to load Transformers.js from all sources."; |
|
|
return; |
|
|
} |
|
|
const src = tries[i]; |
|
|
const s = document.createElement("script"); |
|
|
s.src = src; |
|
|
s.async = true; |
|
|
s.onload = () => { |
|
|
if (window.transformers) { |
|
|
statusEl.className = "ok"; |
|
|
statusEl.textContent = "✅ Transformers.js loaded from: " + src; |
|
|
initApp(); |
|
|
} else { |
|
|
statusEl.textContent = "⚠️ Loaded but window.transformers missing: " + src; |
|
|
tryNext(i + 1); |
|
|
} |
|
|
}; |
|
|
s.onerror = () => { |
|
|
statusEl.textContent = "⚠️ Failed: " + src + " — trying next…"; |
|
|
tryNext(i + 1); |
|
|
}; |
|
|
document.head.appendChild(s); |
|
|
} |
|
|
tryNext(0); |
|
|
|
|
|
|
|
|
function initApp() { |
|
|
const btn = document.getElementById("submit"); |
|
|
const out = document.getElementById("to"); |
|
|
const fromEl = document.getElementById("from"); |
|
|
const srcEl = document.getElementById("src_lang"); |
|
|
const tgtEl = document.getElementById("tgt_lang"); |
|
|
const wrap = document.getElementById("progress-wrap"); |
|
|
const bar = document.getElementById("loadProgress"); |
|
|
const txt = document.getElementById("progressText"); |
|
|
|
|
|
btn.disabled = false; |
|
|
btn.value = "Translate"; |
|
|
|
|
|
const tf = window.transformers; |
|
|
let translator = null; |
|
|
|
|
|
function showProgressUI(show, message) { |
|
|
wrap.style.display = show ? "" : "none"; |
|
|
if (message) txt.textContent = message; |
|
|
bar.value = 0; bar.max = 1; |
|
|
} |
|
|
|
|
|
function progressCallback(p) { |
|
|
if (p.status) txt.textContent = p.status; |
|
|
if (typeof p.loaded === "number" && typeof p.total === "number" && p.total > 0) { |
|
|
bar.max = p.total; |
|
|
bar.value = p.loaded; |
|
|
const pct = Math.round((p.loaded / p.total) * 100); |
|
|
txt.textContent = `Downloading ${p.file || "model"}… ${pct}%`; |
|
|
} |
|
|
} |
|
|
|
|
|
async function getTranslator() { |
|
|
if (translator) return translator; |
|
|
|
|
|
showProgressUI(true, "Loading model…"); |
|
|
try { |
|
|
const { pipeline, env } = tf; |
|
|
env.useBrowserCache = true; |
|
|
env.allowLocalModels = false; |
|
|
|
|
|
env.backends.onnx.wasm.numThreads = Math.max(4, Math.min(8, navigator.hardwareConcurrency || 4)); |
|
|
|
|
|
translator = await pipeline("translation", "Xenova/nllb-200-distilled-600M", { |
|
|
device: (navigator.gpu ? "webgpu" : "wasm"), |
|
|
progress_callback: progressCallback |
|
|
}); |
|
|
|
|
|
txt.textContent = "✅ Model ready"; |
|
|
bar.max = 1; bar.value = 1; |
|
|
return translator; |
|
|
} catch (e) { |
|
|
console.error(e); |
|
|
txt.textContent = "❌ Error loading model — see console."; |
|
|
throw e; |
|
|
} |
|
|
} |
|
|
|
|
|
btn.addEventListener("click", async () => { |
|
|
const prev = btn.value; |
|
|
btn.disabled = true; btn.value = "Working…"; |
|
|
try { |
|
|
const t = await getTranslator(); |
|
|
const text = (fromEl.value || "").trim(); |
|
|
if (!text) { out.value = "Please type some text."; return; } |
|
|
const res = await t(text, { src_lang: srcEl.value, tgt_lang: tgtEl.value, max_length: 128 }); |
|
|
out.value = res?.[0]?.translation_text || "(no output)"; |
|
|
} catch (e) { |
|
|
out.value = "❌ Translation failed. See console."; |
|
|
} finally { |
|
|
btn.disabled = false; btn.value = prev; |
|
|
} |
|
|
}); |
|
|
} |
|
|
})(); |
|
|
</script> |
|
|
</body> |
|
|
</html> |