File size: 6,205 Bytes
7a28584
cba6b29
 
 
 
26f161e
cba6b29
26f161e
 
 
 
 
 
 
 
cba6b29
 
 
 
 
 
26f161e
cba6b29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26f161e
cba6b29
 
 
 
 
 
 
 
26f161e
cba6b29
 
26f161e
cba6b29
 
26f161e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7a28584
cba6b29
26f161e
 
cba6b29
 
26f161e
 
cba6b29
 
26f161e
 
 
cba6b29
26f161e
cba6b29
 
26f161e
 
 
 
 
 
 
 
cba6b29
26f161e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cba6b29
 
 
 
 
 
26f161e
 
 
 
 
 
cba6b29
 
 
 
 
 
 
 
26f161e
 
 
 
 
 
 
cba6b29
 
 
 
 
 
 
 
 
 
 
 
26f161e
cba6b29
 
 
 
 
26f161e
cba6b29
 
7a28584
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
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1"/>
  <title>NLLB translation in browser</title>
  <style>
    body { font:14px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin:16px; }
    h1 { margin:0 0 12px }
    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 }
    .muted { opacity:.8 }
    .ok { color:#0a0 } .err{ color:#b00 } .warn{ color:#b66 }
  </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="status" class="muted" style="margin-top:8px">Loading library…</div>

  <script>
    const statusEl = document.getElementById('status');

    async function loadTransformers() {
      // Try ESM from CDNs (like julien-c’s page)
      const cdns = [
        'https://cdn.jsdelivr.net/npm/@xenova/transformers@3.0.0',
        'https://unpkg.com/@xenova/transformers@3.0.0'
      ];
      for (const url of cdns) {
        try {
          statusEl.textContent = `Trying ESM: ${url}`;
          const mod = await import(/* @vite-ignore */ url);
          if (mod?.pipeline) {
            window.transformers = mod; // expose
            statusEl.textContent = `✅ Loaded via ESM: ${url}`;
            return;
          }
        } catch (_) {}
      }

      // Fallback: local UMD (needs real ~3–4 MB bundle in repo root)
      statusEl.textContent = 'Trying local UMD ./transformers.min.js…';
      await new Promise((resolve) => {
        const s = document.createElement('script');
        s.src = './transformers.min.js?v=' + Math.random();
        s.onload = resolve; s.onerror = resolve;
        document.head.appendChild(s);
      });
      if (window.transformers?.pipeline) {
        statusEl.textContent = '✅ Loaded via local UMD (./transformers.min.js)';
        return;
      }
      statusEl.textContent = '❌ Could not load Transformers.js (ESM or UMD).';
    }

    function showProgressUI(show, message) {
      const wrap = document.getElementById("progress-wrap");
      const txt = document.getElementById("progressText");
      const bar = document.getElementById("loadProgress");
      wrap.style.display = show ? "" : "none";
      if (message) txt.textContent = message;
      bar.value = 0; bar.max = 1;
    }

    function progressCallback(p) {
      const txt = document.getElementById("progressText");
      const bar = document.getElementById("loadProgress");
      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 () => {
      await loadTransformers();
      const tf = window.transformers;
      if (!(tf && tf.pipeline)) return;

      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");

      // env setup
      const { env } = tf;
      env.useBrowserCache = true;
      env.allowLocalModels = false;
      env.backends.onnx.wasm.numThreads = Math.max(4, Math.min(8, navigator.hardwareConcurrency || 4));

      btn.disabled = false;
      btn.value = "Translate";

      let translator = null;
      async function getTranslator() {
        if (translator) return translator;
        showProgressUI(true, "Loading model…");
        translator = await tf.pipeline("translation", "Xenova/nllb-200-distilled-600M", {
          device: (navigator.gpu ? "webgpu" : "wasm"),
          progress_callback: progressCallback
        });
        document.getElementById("progressText").textContent = "✅ Model ready";
        const bar = document.getElementById("loadProgress"); bar.max = 1; bar.value = 1;
        return translator;
      }

      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) {
          console.error(e);
          out.value = "❌ Translation failed. See console.";
        } finally {
          btn.disabled = false; btn.value = prev;
        }
      });
    })();
  </script>
</body>
</html>