File size: 6,764 Bytes
bb12dec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
01723a1
 
bb12dec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
01723a1
bb12dec
01723a1
bb12dec
01723a1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ff7fea2
01723a1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bb12dec
 
01723a1
bb12dec
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
<!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>

  <!-- Progress UI -->
  <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>

  <!-- Loader that tries multiple CDNs, then local file -->
  <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"  // <- upload this file as a final fallback
      ];
      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);

      // initApp runs only after the library is present
      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;
            // If WASM fallback happens, more threads help a bit
            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>