Spaces:
Running
Running
moxhi 0454a43
Browse files- README.md +2 -0
- apple-touch-icon.png +0 -0
- assets/main-CS5AEj4z.js +124 -0
- assets/main-CS5AEj4z.js.map +1 -0
- icon-192.png +0 -0
- icon-512.png +0 -0
- icon-maskable-512.png +0 -0
- index.html +4 -1
- manifest.webmanifest +17 -0
- sw.js +87 -0
README.md
CHANGED
|
@@ -22,7 +22,9 @@ biến thể subgroup an toàn).
|
|
| 22 |
**Tính năng:**
|
| 23 |
- 2 model zh→vi (Mặc Hi d448/8×56, Hachimi d576/8×72) — chuyển đổi ngay trong app
|
| 24 |
- Tự động chuyển phồn thể → giản thể (dữ liệu OpenCC) trước khi dịch
|
|
|
|
| 25 |
- Batch decode + KV cache, chạy được cả trên GPU điện thoại (Adreno)
|
|
|
|
| 26 |
- Khôi phục sau device-lost, resume giữa chừng, wake lock khi đang dịch
|
| 27 |
|
| 28 |
**Yêu cầu:** trình duyệt hỗ trợ WebGPU (Chrome/Edge 121+, Chrome Android).
|
|
|
|
| 22 |
**Tính năng:**
|
| 23 |
- 2 model zh→vi (Mặc Hi d448/8×56, Hachimi d576/8×72) — chuyển đổi ngay trong app
|
| 24 |
- Tự động chuyển phồn thể → giản thể (dữ liệu OpenCC) trước khi dịch
|
| 25 |
+
- Tokenizer SPM-BPE tự viết (không dùng transformers.js) — toàn bộ pipeline tự viết
|
| 26 |
- Batch decode + KV cache, chạy được cả trên GPU điện thoại (Adreno)
|
| 27 |
+
- PWA: cài như app, dịch **offline** sau lần tải model đầu tiên
|
| 28 |
- Khôi phục sau device-lost, resume giữa chừng, wake lock khi đang dịch
|
| 29 |
|
| 30 |
**Yêu cầu:** trình duyệt hỗ trợ WebGPU (Chrome/Edge 121+, Chrome Android).
|
apple-touch-icon.png
ADDED
|
|
assets/main-CS5AEj4z.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import"./modulepreload-polyfill-B5Qt9EMX.js";import{M as j,m as U,D as N,c as V}from"./engine-BTyy_WlH.js";import"./device-Z_ibNtmX.js";import"./weights-B-okS09o.js";import"./shapes-BCQ9D81L.js";import"./constants-C8VbFpic.js";import"./f16-wKKZr58e.js";import"./tokenizer-Ex1u7wXD.js";import"./generate-C4ruG1zm.js";import"./encoder-DX8MOfcH.js";import"./pipelines-DD24nEJg.js";import"./decoder-DI6VDxxO.js";import"./autotune-DBJGFXNj.js";const X="/assets/hero-DgsFBh_e.webp",A="'Songti SC','SimSun','Noto Serif CJK SC','Noto Serif TC',serif",Y=`
|
| 2 |
+
<canvas class="petals" data-ref="petals" aria-hidden="true"></canvas>
|
| 3 |
+
<div class="mist mist-a" aria-hidden="true"></div>
|
| 4 |
+
<div class="mist mist-b" aria-hidden="true"></div>
|
| 5 |
+
<div class="page">
|
| 6 |
+
<header class="site">
|
| 7 |
+
<div class="brand">
|
| 8 |
+
<svg class="seal" viewBox="0 0 64 64" role="img" aria-label="Ấn 墨曦">
|
| 9 |
+
<rect x="1.5" y="1.5" width="61" height="61" rx="7" fill="var(--seal)"/>
|
| 10 |
+
<rect x="5" y="5" width="54" height="54" rx="4" fill="none" stroke="rgba(244,238,227,.55)" stroke-width="1.5"/>
|
| 11 |
+
<text x="32" y="29" font-size="23" text-anchor="middle" fill="var(--paper)" font-family="${A}">墨</text>
|
| 12 |
+
<text x="32" y="55" font-size="23" text-anchor="middle" fill="var(--paper)" font-family="${A}">曦</text>
|
| 13 |
+
</svg>
|
| 14 |
+
<div class="brand-text">
|
| 15 |
+
<h1>Mặc Hi</h1>
|
| 16 |
+
<p class="tagline">Dịch truyện Trung – Việt · chạy trọn trong trình duyệt</p>
|
| 17 |
+
</div>
|
| 18 |
+
</div>
|
| 19 |
+
<svg class="brushline" viewBox="0 0 600 12" preserveAspectRatio="none" aria-hidden="true">
|
| 20 |
+
<path d="M2 7 Q120 1 300 6 T 598 5" fill="none" stroke="var(--ink)" stroke-width="2.6"
|
| 21 |
+
stroke-linecap="round" opacity="0.55"/>
|
| 22 |
+
<path d="M8 9 Q160 5 340 8 T 590 8" fill="none" stroke="var(--ink)" stroke-width="1"
|
| 23 |
+
stroke-linecap="round" opacity="0.25"/>
|
| 24 |
+
</svg>
|
| 25 |
+
</header>
|
| 26 |
+
|
| 27 |
+
<main>
|
| 28 |
+
<section class="gate card" data-ref="gate" hidden>
|
| 29 |
+
<h2>Cần WebGPU để vận công</h2>
|
| 30 |
+
<p>Trình duyệt này chưa mở được WebGPU — linh lực không thể lưu chuyển.</p>
|
| 31 |
+
<p class="hint">Hãy dùng <b>Chrome / Edge 113+</b> (Windows, macOS, Android)
|
| 32 |
+
hoặc <b>Safari 26+</b> (macOS, iPhone, iPad).</p>
|
| 33 |
+
<p class="hint" data-ref="gateErr"></p>
|
| 34 |
+
</section>
|
| 35 |
+
|
| 36 |
+
<section class="loading" data-ref="loading">
|
| 37 |
+
<div class="hero" data-ref="hero" aria-hidden="true">
|
| 38 |
+
<img src="${X}" alt="" loading="eager" decoding="async">
|
| 39 |
+
</div>
|
| 40 |
+
<p class="phase" data-ref="phaseLabel">Chuẩn bị…</p>
|
| 41 |
+
<div class="brushbar" role="progressbar" aria-label="Tiến độ tải">
|
| 42 |
+
<div class="brushfill" data-ref="brushFill"></div>
|
| 43 |
+
</div>
|
| 44 |
+
<p class="hint center" data-ref="loadNote">Lần đầu tải ~74 MB trọng số —
|
| 45 |
+
các lần sau mở nhanh nhờ bộ nhớ đệm của trình duyệt.</p>
|
| 46 |
+
</section>
|
| 47 |
+
|
| 48 |
+
<section class="studio" data-ref="studio" hidden>
|
| 49 |
+
<div class="panes">
|
| 50 |
+
<div class="pane src">
|
| 51 |
+
<div class="pane-head"><span class="lang">中文</span><span class="count" data-ref="srcCount">0 ký tự</span></div>
|
| 52 |
+
<textarea data-ref="src" lang="zh" spellcheck="false"
|
| 53 |
+
placeholder="山重水复疑无路,柳暗花明又一村。"></textarea>
|
| 54 |
+
<div class="pane-foot">
|
| 55 |
+
<button class="ghost" data-ref="pasteBtn" type="button">Dán</button>
|
| 56 |
+
<button class="ghost" data-ref="clearBtn" type="button">Xóa</button>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
<div class="pane dst">
|
| 60 |
+
<div class="pane-head"><span class="lang">Tiếng Việt</span><span class="count" data-ref="dstNote"></span></div>
|
| 61 |
+
<div class="output" data-ref="out" aria-live="polite"></div>
|
| 62 |
+
<div class="pane-foot">
|
| 63 |
+
<button class="ghost" data-ref="copyBtn" type="button">Chép</button>
|
| 64 |
+
<button class="ghost" data-ref="saveBtn" type="button">Tải .txt</button>
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
</div>
|
| 68 |
+
|
| 69 |
+
<div class="actions">
|
| 70 |
+
<button class="go" data-ref="goBtn" type="button">Dịch <span class="kbd">Ctrl ⏎</span></button>
|
| 71 |
+
<label class="auto"><input type="checkbox" data-ref="autoTgl"> Tự dịch khi ngừng gõ</label>
|
| 72 |
+
<label class="encpick modelpick">Mô hình
|
| 73 |
+
<select data-ref="modelSelect"></select>
|
| 74 |
+
</label>
|
| 75 |
+
<span class="progress" data-ref="progress" aria-live="polite"></span>
|
| 76 |
+
</div>
|
| 77 |
+
|
| 78 |
+
<div class="filezone">
|
| 79 |
+
<input type="file" accept=".txt,text/plain" data-ref="fileInput" hidden>
|
| 80 |
+
<div class="drop" data-ref="drop">
|
| 81 |
+
<p>Kéo thả file <b>.txt</b> vào đây, hoặc
|
| 82 |
+
<button class="linklike" data-ref="browseBtn" type="button">chọn file</button></p>
|
| 83 |
+
<p class="hint">Tự nhận diện mã hóa GBK / Big5 / UTF-8 · tối đa 20 MB</p>
|
| 84 |
+
</div>
|
| 85 |
+
<div class="filecard card" data-ref="filecard" hidden>
|
| 86 |
+
<div class="filemeta">
|
| 87 |
+
<b data-ref="fileName"></b>
|
| 88 |
+
<span class="hint" data-ref="fileSize"></span>
|
| 89 |
+
<label class="encpick">Mã hóa
|
| 90 |
+
<select data-ref="encSelect"></select>
|
| 91 |
+
</label>
|
| 92 |
+
<button class="ghost" data-ref="fileDropBtn" type="button">Bỏ file</button>
|
| 93 |
+
</div>
|
| 94 |
+
<pre class="preview" data-ref="filePreview"></pre>
|
| 95 |
+
<div class="fileacts">
|
| 96 |
+
<button class="go small" data-ref="fileGoBtn" type="button">Dịch file</button>
|
| 97 |
+
<button class="ghost" data-ref="fileCancelBtn" type="button" hidden>Hủy</button>
|
| 98 |
+
<span class="progress" data-ref="fileProgress" aria-live="polite"></span>
|
| 99 |
+
<button class="go small alt" data-ref="fileSaveBtn" type="button" hidden>Tải bản dịch (.txt)</button>
|
| 100 |
+
</div>
|
| 101 |
+
<pre class="preview result" data-ref="fileResult" hidden></pre>
|
| 102 |
+
</div>
|
| 103 |
+
</div>
|
| 104 |
+
</section>
|
| 105 |
+
|
| 106 |
+
<div class="error-banner" data-ref="errBanner" hidden>
|
| 107 |
+
<span data-ref="errMsg"></span>
|
| 108 |
+
<button class="ghost" data-ref="retryBtn" type="button">Thử lại</button>
|
| 109 |
+
</div>
|
| 110 |
+
</main>
|
| 111 |
+
|
| 112 |
+
<footer class="stats">
|
| 113 |
+
<span data-ref="statGpu"></span>
|
| 114 |
+
<span data-ref="statPerf"></span>
|
| 115 |
+
<span class="privacy">100% trên máy bạn — văn bản không rời khỏi thiết bị</span>
|
| 116 |
+
<a class="credit" href="https://huggingface.co/DanVP/MoxhiMT-30" target="_blank"
|
| 117 |
+
rel="noopener">mô hình MoxhiMT-30</a>
|
| 118 |
+
</footer>
|
| 119 |
+
</div>`;function Q(t){t.innerHTML=Y;const n={};for(const a of t.querySelectorAll("[data-ref]"))n[a.dataset.ref]=a;return n}const J=["auto","utf-8","gb18030","big5","utf-16le","utf-16be"],Z=new Set("的一是不了人我在有他这为之大来以个中上们到说国和地也子时道出而要于就下得可你年生自会那后能对着事其里所去行过家十用发天如然作方成者多日都三小军二无同么经法当起与好看学进种将还分此心前面又定见只主没公从知世什两次使身被高已亲话常活正感明问力理尔点文几本特做外孩相西果走月实向声车全信重机工物气每并别真打太新比才便夫再书部水像眼等体却加电主界门利海受听表德少克代员许先口由死安写性马光白或住难望教命花结乐色更拉东神记处让母父应直字场平报友关放至张认接告入笑内英军候民岁往何度山觉路带万男边风解叫任金快原吃妈变通师立象数四失满战远格士音轻目条呢病始达深完今提求清王化空业思切怎非找片罗钱吗语元喜曾离飞科言干流欢约各即指合反题必该论交终林请医晚制球决传画保读运及则房早院量苦火布品近坐产答星精视五连司巴這為來個們說國時過還進裡著見兩麼開關門問間東車書興學實現點戰處條讓報馬場話認遠飛樂氣師難聽當義務長員島洋歐陽於億敵獨隊淚類燈爾談請諸講謝證識護賽選遺醫錢鐵閒陳陰險陸雙雞離電靈頭頗題顧風餘體高鬥魚鳥龍發夢舊聲應轉醒覺對藥雲會後".split("")).add(",").add("。").add("、"),S=(t,...n)=>n.every((a,i)=>t[i]===a);function R(t){let n=0,a=0,i=0;for(const l of t){const o=l.codePointAt(0);if(o===65533){i++;continue}(o>=19968&&o<=40959||o>=12288&&o<=12351||o>=65280&&o<=65374)&&(n++,Z.has(l)&&a++)}return n+i===0?-1:(a-4*i)/(n+i)}function ee(t){if(t.length>=3&&S(t,239,187,191))return"utf-8";if(t.length>=2&&S(t,255,254))return"utf-16le";if(t.length>=2&&S(t,254,255))return"utf-16be";try{return new TextDecoder("utf-8",{fatal:!0}).decode(t),"utf-8"}catch{}const n=t.subarray(0,65536),a=new TextDecoder("gb18030").decode(n),i=new TextDecoder("big5").decode(n),l=R(a),o=R(i);return o>l*1.05&&o>0?"big5":"gb18030"}function I(t,n="auto"){const a=n==="auto"?ee(t):n;return{text:new TextDecoder(a).decode(t),encoding:a}}function te(t,{reduced:n=!1}={}){if(n||!t.getContext)return t.remove(),()=>{};const a=t.getContext("2d"),i=Math.min(devicePixelRatio||1,2),l=document.createElement("canvas");l.width=l.height=32;const o=l.getContext("2d");o.translate(16,16);const h=o.createRadialGradient(0,-2,1,0,0,14);h.addColorStop(0,"rgba(246,219,231,0.95)"),h.addColorStop(1,"rgba(214,123,166,0.72)"),o.fillStyle=h,o.beginPath(),o.moveTo(0,-12),o.quadraticCurveTo(10,-6,0,12),o.quadraticCurveTo(-10,-6,0,-12),o.fill();let d,r,f;const k=()=>innerWidth<600?12:24,F=b=>({x:Math.random()*d,y:b?-24:Math.random()*r,vy:18+Math.random()*26,drift:10+Math.random()*24,phase:Math.random()*Math.PI*2,rot:Math.random()*Math.PI*2,vr:(Math.random()-.5)*1.6,size:8+Math.random()*10}),D=()=>{d=innerWidth,r=innerHeight,t.width=d*i,t.height=r*i,t.style.width=`${d}px`,t.style.height=`${r}px`,a.setTransform(i,0,0,i,0,0),f=Array.from({length:k()},()=>F(!1))};D(),addEventListener("resize",D);let E=performance.now(),v=0;const T=b=>{const w=Math.min(.05,(b-E)/1e3);E=b,a.clearRect(0,0,d,r);for(let B=0;B<f.length;B++){const u=f[B];u.y+=u.vy*w,u.x+=Math.sin(b/1e3+u.phase)*u.drift*w,u.rot+=u.vr*w,u.y>r+24&&(f[B]=F(!0)),a.save(),a.translate(u.x,u.y),a.rotate(u.rot),a.globalAlpha=.8,a.drawImage(l,-u.size/2,-u.size/2,u.size,u.size),a.restore()}v=requestAnimationFrame(T)};return v=requestAnimationFrame(T),document.addEventListener("visibilitychange",()=>{document.hidden?cancelAnimationFrame(v):(E=performance.now(),v=requestAnimationFrame(T))}),()=>cancelAnimationFrame(v)}function ne(t,n,{reduced:a=!1}={}){if(a){t.textContent=n;return}t.replaceChildren();let i=0;for(const l of n.split(`
|
| 120 |
+
`)){i>0&&t.appendChild(document.createTextNode(`
|
| 121 |
+
`));const o=l.split(/(?<=[.!?…:;])\s+/);for(const h of o){if(!h)continue;const d=document.createElement("span");d.className="ink-in",d.style.animationDelay=`${Math.min(i*40,1200)}ms`,d.textContent=`${h} `,t.appendChild(d),i++}}}function _(t){t&&t.addEventListener("click",n=>{const a=t.getBoundingClientRect(),i=document.createElement("span");i.className="ripple",i.style.left=`${n.clientX-a.left||a.width/2}px`,i.style.top=`${n.clientY-a.top||a.height/2}px`,t.appendChild(i),i.addEventListener("animationend",()=>i.remove())})}const ae=20*1024*1024,se=600,ie=900,z="moxhi.autotranslate",P="moxhi.model",$={auto:"Tự động","utf-8":"UTF-8",gb18030:"GB18030 / GBK",big5:"Big5","utf-16le":"UTF-16 LE","utf-16be":"UTF-16 BE"},O=matchMedia("(prefers-reduced-motion: reduce)").matches,e=Q(document.querySelector("#app"));te(e.petals,{reduced:O});_(e.goBtn);_(e.fileGoBtn);const s={engine:null,busy:!1,queued:!1,stopFlag:!1,file:null,fileResultText:null,filePartial:null};let x=null;async function q(){try{x=await navigator.wakeLock?.request("screen")??null}catch{x=null}}function re(){x?.release().catch(()=>{}),x=null}document.addEventListener("visibilitychange",()=>{document.visibilityState==="visible"&&s.busy&&!x&&q()});const c=(t,n=!0)=>{t.hidden=!n},y=t=>(t/1048576).toFixed(1),M=t=>t<950?`${Math.round(t)}ms`:`${(t/1e3).toFixed(1)}s`;function p(t,n){e.errMsg.textContent=t,c(e.errBanner),e.retryBtn.onclick=()=>{c(e.errBanner,!1),n()}}const oe={gpu:"Gọi linh khí GPU…",weights:"Tải trọng số…",tokenizer:"Nạp từ điển…",autotune:"Đo mạch thiết bị (chỉ lần đầu)…"};async function m(){c(e.gate,!1),c(e.studio,!1),c(e.loading,!0);const t=U(localStorage.getItem(P)??N);e.modelSelect.value=t.id,e.modelSelect.disabled=!0,e.loadNote.innerHTML=`Lần đầu tải ~${t.downloadMB} MB trọng số (${t.name}) — các lần sau mở nhanh nhờ bộ nhớ đệm của trình duyệt.`;try{s.engine=await V({model:t,onPhase:r=>{r!=="ready"&&(e.phaseLabel.textContent=oe[r]??r,r==="tokenizer"&&(e.brushFill.style.width="100%"))},onProgress:(r,f)=>{const k=f?Math.round(r/f*100):0;e.brushFill.style.width=`${k}%`,e.phaseLabel.textContent=`Tải trọng số — ${y(r)} / ${y(f)} MB (${k}%)`},onDeviceLost:r=>{s.busy||p(`GPU bị ngắt kết nối (${r.reason}): ${r.message||"không rõ nguyên nhân"}. Bấm thử lại để khởi động lại bộ dịch.`,m)}})}catch(r){console.error(r),e.modelSelect.disabled=!1;const f=String(r?.message??r);/WebGPU unavailable|no adapter/i.test(f)?(c(e.loading,!1),e.gateErr.textContent=`Chi tiết: ${f}`,c(e.gate,!0)):p(`Không khởi động được bộ dịch: ${f}`,m);return}e.modelSelect.disabled=!1;const{gpuName:n,dtype:a,lmHeadQ8:i,tunedFrom:l,modelName:o}=s.engine.info,h=l==="cache"?"autotune (đệm)":l==="measured"?"autotune":l==="partial"?"autotune (một phần)":"routing mặc định",d="0454a43";if(e.statGpu.textContent=`${o} · ${n} · ${a}${i?" + lm_head int8":""} · ${h} · ${d}`,/\b(intel|iris|uhd|xe)\b/i.test(n)){const r=document.createElement("div");r.className="gpu-tip",r.textContent="Đang chạy trên GPU tích hợp. Máy có GPU rời? Windows: Settings → Display → Graphics → thêm trình duyệt → High performance, rồi mở lại trình duyệt — tốc độ có thể nhanh hơn hàng chục lần.",e.statGpu.parentElement.appendChild(r)}c(e.loading,!1),c(e.studio,!0),e.src.focus(),window.__moxhi={engine:s.engine,build:d,translate:r=>s.engine.translateText(r)},document.dispatchEvent(new CustomEvent("moxhi:ready"))}function C(t){s.busy=t,e.goBtn.classList.toggle("busy",t),e.goBtn.disabled=t,e.fileGoBtn.disabled=t,e.modelSelect.disabled=t}async function g(){if(!s.engine)return;if(s.busy){s.queued=!0;return}const t=e.src.value;if(!t.trim()){e.out.textContent="",e.progress.textContent="";return}C(!0),e.progress.textContent="đang dịch…";try{const n=await s.engine.translateText(t,{onBatch:(a,i,l)=>{const o=l.filter(Boolean).length;e.progress.textContent=`câu ${o}/${l.length}`,i>1&&(e.out.textContent=l.filter(Boolean).join(" "))}});ne(e.out,n.text,{reduced:O}),e.progress.textContent="",e.statPerf.textContent=`${Math.round(n.stats.tokPerSec)} tok/s · ${M(n.stats.ms)} · ${n.stats.sentences} câu`+(n.stats.t2s?" · phồn→giản":"")}catch(n){if(console.error(n),e.progress.textContent="",s.engine?.isLost?.()){const{reason:a,message:i}=s.engine.isLost();p(`GPU bị ngắt kết nối (${a}): ${i||"không rõ nguyên nhân"}. Thử lại sẽ khởi động lại bộ dịch.`,async()=>{await m(),g()})}else p(`Dịch lỗi: ${n?.message??n}`,g)}finally{C(!1),s.queued&&(s.queued=!1,g())}}e.goBtn.addEventListener("click",g);e.src.addEventListener("keydown",t=>{(t.ctrlKey||t.metaKey)&&t.key==="Enter"&&(t.preventDefault(),g())});e.autoTgl.checked=(localStorage.getItem(z)??"1")==="1";e.autoTgl.addEventListener("change",()=>{localStorage.setItem(z,e.autoTgl.checked?"1":"0")});e.modelSelect.replaceChildren(...j.map(t=>{const n=document.createElement("option");return n.value=t.id,n.textContent=t.name,n}));e.modelSelect.value=U(localStorage.getItem(P)??N).id;e.modelSelect.addEventListener("change",()=>{localStorage.setItem(P,e.modelSelect.value),s.filePartial=null,m()});let G=0;e.src.addEventListener("input",()=>{if(e.srcCount.textContent=`${e.src.value.length} ký tự`,clearTimeout(G),!(!e.autoTgl.checked||!s.engine)){if(e.src.value.length>se){e.progress.textContent="văn bản dài — bấm Dịch";return}e.progress.textContent="",G=setTimeout(g,ie)}});e.pasteBtn.addEventListener("click",async()=>{try{e.src.value=await navigator.clipboard.readText(),e.src.dispatchEvent(new Event("input"))}catch{e.src.focus(),e.progress.textContent="trình duyệt chặn đọc clipboard — dán bằng Ctrl+V"}});e.clearBtn.addEventListener("click",()=>{e.src.value="",e.out.textContent="",e.src.dispatchEvent(new Event("input")),e.src.focus()});e.copyBtn.addEventListener("click",async()=>{const t=e.out.textContent;t&&await navigator.clipboard.writeText(t).catch(()=>{})});function H(t,n){const a=document.createElement("a");a.href=URL.createObjectURL(new Blob([n],{type:"text/plain;charset=utf-8"})),a.download=t,a.click(),URL.revokeObjectURL(a.href)}e.saveBtn.addEventListener("click",()=>{const t=e.out.textContent;t&&H("mac-hi.txt",t)});function le(t){e.encSelect.replaceChildren(...J.map(n=>{const a=document.createElement("option");return a.value=n,a.textContent=n==="auto"?`${$.auto} (${$[t]??t})`:$[n],a})),e.encSelect.value="auto"}function W(){e.filePreview.textContent=s.file.text.slice(0,500)+(s.file.text.length>500?" …":"")}async function K(t){if(!t)return;if(t.size>ae){p(`File ${y(t.size)} MB vượt giới hạn 20 MB.`,()=>{});return}const n=new Uint8Array(await t.arrayBuffer()),{text:a,encoding:i}=I(n,"auto");s.file={name:t.name,bytes:n,text:a,encoding:i},s.fileResultText=null,s.filePartial=null,e.fileName.textContent=t.name,e.fileSize.textContent=`${y(t.size)} MB · ${i}`,le(i),W(),c(e.drop,!1),c(e.filecard,!0),c(e.fileSaveBtn,!1),c(e.fileResult,!1),e.fileProgress.textContent=""}e.browseBtn.addEventListener("click",()=>e.fileInput.click());e.fileInput.addEventListener("change",()=>K(e.fileInput.files[0]));for(const[t,n]of[["dragover",!0],["dragleave",!1]])e.drop.addEventListener(t,a=>{a.preventDefault(),e.drop.classList.toggle("dragover",n)});e.drop.addEventListener("drop",t=>{t.preventDefault(),e.drop.classList.remove("dragover"),K(t.dataTransfer.files[0])});e.encSelect.addEventListener("change",()=>{const t=e.encSelect.value,{text:n,encoding:a}=I(s.file.bytes,t);s.file.text=n,s.file.encoding=a,s.filePartial=null,e.fileSize.textContent=`${y(s.file.bytes.length)} MB · ${a}`,W()});e.fileDropBtn.addEventListener("click",()=>{s.file=null,s.fileResultText=null,s.filePartial=null,e.fileInput.value="",c(e.filecard,!1),c(e.drop,!0)});async function L(){if(!s.engine||!s.file||s.busy)return;C(!0),s.stopFlag=!1,c(e.fileCancelBtn,!0),c(e.fileSaveBtn,!1),c(e.fileResult,!1),q();const t=performance.now();try{const n=await s.engine.translateText(s.file.text,{pack:!0,resume:s.filePartial,shouldStop:()=>s.stopFlag,onBatch:(a,i,l)=>{s.filePartial=l;const o=l.filter(Boolean).length,h=Math.round(a/i*100),d=(performance.now()-t)/1e3,r=a>0?Math.max(0,d/a*(i-a)):0;e.fileProgress.textContent=`${o}/${l.length} cụm câu · ${h}% · còn ~${Math.ceil(r)}s`}});n.cancelled||(s.filePartial=null),s.fileResultText=n.text+(n.cancelled?`
|
| 122 |
+
|
| 123 |
+
[— bản dịch dở dang —]`:""),e.fileProgress.textContent=n.cancelled?"đã hủy — phần dịch dở vẫn tải được":`xong — ${M(n.stats.ms)} · ${Math.round(n.stats.tokPerSec)} tok/s`+(n.stats.forcedEos?` · ⚠ ${n.stats.forcedEos} cụm chạm trần token`:""),e.fileResult.textContent=s.fileResultText.slice(0,1e3)+(s.fileResultText.length>1e3?" …":""),c(e.fileResult,!0),c(e.fileSaveBtn,!0),e.statPerf.textContent=`${Math.round(n.stats.tokPerSec)} tok/s · ${M(n.stats.ms)} · ${n.stats.sentences} cụm câu`+(n.stats.t2s?" · phồn→giản":"")}catch(n){console.error(n);const a=s.filePartial?s.filePartial.filter(Boolean).length:0;if(s.engine?.isLost?.()){const{reason:i,message:l}=s.engine.isLost();e.fileProgress.textContent=a?`đã dịch ${a} cụm — sẽ tiếp tục từ đó`:"",p(`GPU bị ngắt kết nối (${i}): ${l||"không rõ nguyên nhân"}. Thử lại sẽ khởi động lại bộ dịch và dịch tiếp từ chỗ dừng.`,async()=>{await m(),L()})}else e.fileProgress.textContent="",p(`Dịch file lỗi: ${n?.message??n}`,L)}finally{re(),c(e.fileCancelBtn,!1),C(!1)}}e.fileGoBtn.addEventListener("click",L);e.fileCancelBtn.addEventListener("click",()=>{s.stopFlag=!0});e.fileSaveBtn.addEventListener("click",()=>{if(!s.fileResultText)return;const t=s.file.name.replace(/\.txt$/i,"");H(`${t}.vi.txt`,s.fileResultText)});m();"serviceWorker"in navigator&&window.addEventListener("load",()=>{navigator.serviceWorker.register("/sw.js").catch(()=>{})});
|
| 124 |
+
//# sourceMappingURL=main-CS5AEj4z.js.map
|
assets/main-CS5AEj4z.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"version":3,"file":"main-CS5AEj4z.js","sources":["../../src/app/assets/hero.webp","../../src/app/ui.js","../../src/app/encoding.js","../../src/app/fx.js","../../src/app/main.js"],"sourcesContent":["export default \"__VITE_ASSET__CaZYHUja__\"","// DOM template + refs for the Mặc Hi app. Pure rendering — all behavior\n// lives in main.js. Every interactive node carries data-ref.\nimport heroUrl from './assets/hero.webp';\n\nconst SEAL_FONT = \"'Songti SC','SimSun','Noto Serif CJK SC','Noto Serif TC',serif\";\n\nconst TEMPLATE = `\n<canvas class=\"petals\" data-ref=\"petals\" aria-hidden=\"true\"></canvas>\n<div class=\"mist mist-a\" aria-hidden=\"true\"></div>\n<div class=\"mist mist-b\" aria-hidden=\"true\"></div>\n<div class=\"page\">\n <header class=\"site\">\n <div class=\"brand\">\n <svg class=\"seal\" viewBox=\"0 0 64 64\" role=\"img\" aria-label=\"Ấn 墨曦\">\n <rect x=\"1.5\" y=\"1.5\" width=\"61\" height=\"61\" rx=\"7\" fill=\"var(--seal)\"/>\n <rect x=\"5\" y=\"5\" width=\"54\" height=\"54\" rx=\"4\" fill=\"none\" stroke=\"rgba(244,238,227,.55)\" stroke-width=\"1.5\"/>\n <text x=\"32\" y=\"29\" font-size=\"23\" text-anchor=\"middle\" fill=\"var(--paper)\" font-family=\"${SEAL_FONT}\">墨</text>\n <text x=\"32\" y=\"55\" font-size=\"23\" text-anchor=\"middle\" fill=\"var(--paper)\" font-family=\"${SEAL_FONT}\">曦</text>\n </svg>\n <div class=\"brand-text\">\n <h1>Mặc Hi</h1>\n <p class=\"tagline\">Dịch truyện Trung – Việt · chạy trọn trong trình duyệt</p>\n </div>\n </div>\n <svg class=\"brushline\" viewBox=\"0 0 600 12\" preserveAspectRatio=\"none\" aria-hidden=\"true\">\n <path d=\"M2 7 Q120 1 300 6 T 598 5\" fill=\"none\" stroke=\"var(--ink)\" stroke-width=\"2.6\"\n stroke-linecap=\"round\" opacity=\"0.55\"/>\n <path d=\"M8 9 Q160 5 340 8 T 590 8\" fill=\"none\" stroke=\"var(--ink)\" stroke-width=\"1\"\n stroke-linecap=\"round\" opacity=\"0.25\"/>\n </svg>\n </header>\n\n <main>\n <section class=\"gate card\" data-ref=\"gate\" hidden>\n <h2>Cần WebGPU để vận công</h2>\n <p>Trình duyệt này chưa mở được WebGPU — linh lực không thể lưu chuyển.</p>\n <p class=\"hint\">Hãy dùng <b>Chrome / Edge 113+</b> (Windows, macOS, Android)\n hoặc <b>Safari 26+</b> (macOS, iPhone, iPad).</p>\n <p class=\"hint\" data-ref=\"gateErr\"></p>\n </section>\n\n <section class=\"loading\" data-ref=\"loading\">\n <div class=\"hero\" data-ref=\"hero\" aria-hidden=\"true\">\n <img src=\"${heroUrl}\" alt=\"\" loading=\"eager\" decoding=\"async\">\n </div>\n <p class=\"phase\" data-ref=\"phaseLabel\">Chuẩn bị…</p>\n <div class=\"brushbar\" role=\"progressbar\" aria-label=\"Tiến độ tải\">\n <div class=\"brushfill\" data-ref=\"brushFill\"></div>\n </div>\n <p class=\"hint center\" data-ref=\"loadNote\">Lần đầu tải ~74 MB trọng số —\n các lần sau mở nhanh nhờ bộ nhớ đệm của trình duyệt.</p>\n </section>\n\n <section class=\"studio\" data-ref=\"studio\" hidden>\n <div class=\"panes\">\n <div class=\"pane src\">\n <div class=\"pane-head\"><span class=\"lang\">中文</span><span class=\"count\" data-ref=\"srcCount\">0 ký tự</span></div>\n <textarea data-ref=\"src\" lang=\"zh\" spellcheck=\"false\"\n placeholder=\"山重水复疑无路,柳暗花明又一村。\"></textarea>\n <div class=\"pane-foot\">\n <button class=\"ghost\" data-ref=\"pasteBtn\" type=\"button\">Dán</button>\n <button class=\"ghost\" data-ref=\"clearBtn\" type=\"button\">Xóa</button>\n </div>\n </div>\n <div class=\"pane dst\">\n <div class=\"pane-head\"><span class=\"lang\">Tiếng Việt</span><span class=\"count\" data-ref=\"dstNote\"></span></div>\n <div class=\"output\" data-ref=\"out\" aria-live=\"polite\"></div>\n <div class=\"pane-foot\">\n <button class=\"ghost\" data-ref=\"copyBtn\" type=\"button\">Chép</button>\n <button class=\"ghost\" data-ref=\"saveBtn\" type=\"button\">Tải .txt</button>\n </div>\n </div>\n </div>\n\n <div class=\"actions\">\n <button class=\"go\" data-ref=\"goBtn\" type=\"button\">Dịch <span class=\"kbd\">Ctrl ⏎</span></button>\n <label class=\"auto\"><input type=\"checkbox\" data-ref=\"autoTgl\"> Tự dịch khi ngừng gõ</label>\n <label class=\"encpick modelpick\">Mô hình\n <select data-ref=\"modelSelect\"></select>\n </label>\n <span class=\"progress\" data-ref=\"progress\" aria-live=\"polite\"></span>\n </div>\n\n <div class=\"filezone\">\n <input type=\"file\" accept=\".txt,text/plain\" data-ref=\"fileInput\" hidden>\n <div class=\"drop\" data-ref=\"drop\">\n <p>Kéo thả file <b>.txt</b> vào đây, hoặc\n <button class=\"linklike\" data-ref=\"browseBtn\" type=\"button\">chọn file</button></p>\n <p class=\"hint\">Tự nhận diện mã hóa GBK / Big5 / UTF-8 · tối đa 20 MB</p>\n </div>\n <div class=\"filecard card\" data-ref=\"filecard\" hidden>\n <div class=\"filemeta\">\n <b data-ref=\"fileName\"></b>\n <span class=\"hint\" data-ref=\"fileSize\"></span>\n <label class=\"encpick\">Mã hóa\n <select data-ref=\"encSelect\"></select>\n </label>\n <button class=\"ghost\" data-ref=\"fileDropBtn\" type=\"button\">Bỏ file</button>\n </div>\n <pre class=\"preview\" data-ref=\"filePreview\"></pre>\n <div class=\"fileacts\">\n <button class=\"go small\" data-ref=\"fileGoBtn\" type=\"button\">Dịch file</button>\n <button class=\"ghost\" data-ref=\"fileCancelBtn\" type=\"button\" hidden>Hủy</button>\n <span class=\"progress\" data-ref=\"fileProgress\" aria-live=\"polite\"></span>\n <button class=\"go small alt\" data-ref=\"fileSaveBtn\" type=\"button\" hidden>Tải bản dịch (.txt)</button>\n </div>\n <pre class=\"preview result\" data-ref=\"fileResult\" hidden></pre>\n </div>\n </div>\n </section>\n\n <div class=\"error-banner\" data-ref=\"errBanner\" hidden>\n <span data-ref=\"errMsg\"></span>\n <button class=\"ghost\" data-ref=\"retryBtn\" type=\"button\">Thử lại</button>\n </div>\n </main>\n\n <footer class=\"stats\">\n <span data-ref=\"statGpu\"></span>\n <span data-ref=\"statPerf\"></span>\n <span class=\"privacy\">100% trên máy bạn — văn bản không rời khỏi thiết bị</span>\n <a class=\"credit\" href=\"https://huggingface.co/DanVP/MoxhiMT-30\" target=\"_blank\"\n rel=\"noopener\">mô hình MoxhiMT-30</a>\n </footer>\n</div>`;\n\nexport function renderApp(root) {\n root.innerHTML = TEMPLATE;\n const refs = {};\n for (const el of root.querySelectorAll('[data-ref]')) refs[el.dataset.ref] = el;\n return refs;\n}\n","// File-encoding detection for .txt novel files. Browsers ship gb18030/big5\n// TextDecoders per the Encoding Standard, so no library is needed. Strategy:\n// BOM → strict utf-8 probe → gb18030-vs-big5 scored by how much of the\n// decoded CJK text falls in a common-hanzi set (a wrong decode of real prose\n// yields mostly rare characters, kana, or Cyrillic).\nexport const ENCODING_CHOICES = ['auto', 'utf-8', 'gb18030', 'big5', 'utf-16le', 'utf-16be'];\n\n// Highest-frequency hanzi, simplified + traditional forms merged (duplicates\n// are harmless in a Set). Wrong-codec decodes of real prose land almost\n// entirely outside this set.\nconst COMMON_HANZI = new Set(('的一是不了人我在有他这为之大来以个中上们到说国和地也子时道出而要于就下得可你年生自会那后能对着事其里所去行过家十用发天如然作方成者多日都三小军二无同么经法当起与好看学进种将还分此心前面又定见只主没公从知世什两次使身被高已亲话常活正感明问力理尔点文几本特做外孩相西果走月实向声车全信重机工物气每并别真打太新比才便夫再书部水像眼等体却加电主界门利海受听表德少克代员许先口由死安写性马光白或住难望教命花结乐色更拉东神记处让母父应直字场平报友关放至张认接告入笑内英军候民岁往何度山觉路带万男边风解叫任金快原吃妈变通师立象数四失满战远格士音轻目条呢病始达深完今提求清王化空业思切怎非找片罗钱吗语元喜曾离飞科言干流欢约各即指合反题必该论交终林请医晚制球决传画保读运及则房早院量苦火布品近坐产答星精视五连司巴' +\n '這為來個們說國時過還進裡著見兩麼開關門問間東車書興學實現點戰處條讓報馬場話認遠飛樂氣師難聽當義務長員島洋歐陽於億敵獨隊淚類燈爾談請諸講謝證識護賽選遺醫錢鐵閒陳陰險陸雙雞離電靈頭頗題顧風餘體高鬥魚鳥龍發夢舊聲應轉醒覺對藥雲會後').split('')).add(',').add('。').add('、');\n\nconst hasBOM = (b, ...sig) => sig.every((v, i) => b[i] === v);\n\nfunction scoreCJK(text) {\n let cjk = 0;\n let common = 0;\n let bad = 0;\n for (const ch of text) {\n const c = ch.codePointAt(0);\n if (c === 0xFFFD) { bad++; continue; }\n if ((c >= 0x4E00 && c <= 0x9FFF) || (c >= 0x3000 && c <= 0x303F) || (c >= 0xFF00 && c <= 0xFF5E)) {\n cjk++;\n if (COMMON_HANZI.has(ch)) common++;\n }\n }\n if (cjk + bad === 0) return -1;\n return (common - 4 * bad) / (cjk + bad);\n}\n\nexport function sniffEncoding(bytes) {\n if (bytes.length >= 3 && hasBOM(bytes, 0xEF, 0xBB, 0xBF)) return 'utf-8';\n if (bytes.length >= 2 && hasBOM(bytes, 0xFF, 0xFE)) return 'utf-16le';\n if (bytes.length >= 2 && hasBOM(bytes, 0xFE, 0xFF)) return 'utf-16be';\n try {\n new TextDecoder('utf-8', { fatal: true }).decode(bytes);\n return 'utf-8';\n } catch { /* not utf-8 — fall through to legacy CJK codecs */ }\n // Score on a sample (first 64KB) — plenty for prose, cheap for big files.\n const sample = bytes.subarray(0, 65536);\n const gb = new TextDecoder('gb18030').decode(sample);\n const b5 = new TextDecoder('big5').decode(sample);\n const sGb = scoreCJK(gb);\n const sB5 = scoreCJK(b5);\n return sB5 > sGb * 1.05 && sB5 > 0 ? 'big5' : 'gb18030';\n}\n\nexport function decodeBytes(bytes, encoding = 'auto') {\n const enc = encoding === 'auto' ? sniffEncoding(bytes) : encoding;\n // TextDecoder strips a leading BOM for utf-8/utf-16 by default.\n const text = new TextDecoder(enc).decode(bytes);\n return { text, encoding: enc };\n}\n","// Visual effects for the Mặc Hi app: falling blossom petals, ink-soak text\n// reveal, and an ink ripple on buttons. Everything degrades to static under\n// prefers-reduced-motion and pauses with the tab.\n\n// Falling petals on a fixed full-viewport canvas. One petal sprite is\n// pre-rendered offscreen and stamped with per-petal transforms — cheap enough\n// for phones.\nexport function startPetals(canvas, { reduced = false } = {}) {\n if (reduced || !canvas.getContext) { canvas.remove(); return () => {}; }\n const ctx = canvas.getContext('2d');\n const dpr = Math.min(devicePixelRatio || 1, 2);\n\n const sprite = document.createElement('canvas');\n sprite.width = sprite.height = 32;\n const s = sprite.getContext('2d');\n s.translate(16, 16);\n const g = s.createRadialGradient(0, -2, 1, 0, 0, 14);\n g.addColorStop(0, 'rgba(246,219,231,0.95)');\n g.addColorStop(1, 'rgba(214,123,166,0.72)');\n s.fillStyle = g;\n s.beginPath(); // petal: two quadratic lobes\n s.moveTo(0, -12);\n s.quadraticCurveTo(10, -6, 0, 12);\n s.quadraticCurveTo(-10, -6, 0, -12);\n s.fill();\n\n let W, H, petals;\n const count = () => (innerWidth < 600 ? 12 : 24);\n const spawn = (top) => ({\n x: Math.random() * W,\n y: top ? -24 : Math.random() * H,\n vy: 18 + Math.random() * 26, // px/s\n drift: 10 + Math.random() * 24,\n phase: Math.random() * Math.PI * 2,\n rot: Math.random() * Math.PI * 2,\n vr: (Math.random() - 0.5) * 1.6,\n size: 8 + Math.random() * 10,\n });\n const resize = () => {\n W = innerWidth;\n H = innerHeight;\n canvas.width = W * dpr;\n canvas.height = H * dpr;\n canvas.style.width = `${W}px`;\n canvas.style.height = `${H}px`;\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n petals = Array.from({ length: count() }, () => spawn(false));\n };\n resize();\n addEventListener('resize', resize);\n\n let last = performance.now();\n let raf = 0;\n const tick = (now) => {\n const dt = Math.min(0.05, (now - last) / 1000);\n last = now;\n ctx.clearRect(0, 0, W, H);\n for (let i = 0; i < petals.length; i++) {\n const p = petals[i];\n p.y += p.vy * dt;\n p.x += Math.sin(now / 1000 + p.phase) * p.drift * dt;\n p.rot += p.vr * dt;\n if (p.y > H + 24) petals[i] = spawn(true);\n ctx.save();\n ctx.translate(p.x, p.y);\n ctx.rotate(p.rot);\n ctx.globalAlpha = 0.8;\n ctx.drawImage(sprite, -p.size / 2, -p.size / 2, p.size, p.size);\n ctx.restore();\n }\n raf = requestAnimationFrame(tick);\n };\n raf = requestAnimationFrame(tick);\n document.addEventListener('visibilitychange', () => {\n if (document.hidden) cancelAnimationFrame(raf);\n else { last = performance.now(); raf = requestAnimationFrame(tick); }\n });\n return () => cancelAnimationFrame(raf);\n}\n\n// Replace el's content with the text revealed sentence-by-sentence through an\n// ink-soak (blur→sharp) stagger. Paragraph structure (\\n) is preserved.\nexport function inkRevealText(el, text, { reduced = false } = {}) {\n if (reduced) { el.textContent = text; return; }\n el.replaceChildren();\n let i = 0;\n for (const para of text.split('\\n')) {\n if (i > 0) el.appendChild(document.createTextNode('\\n'));\n const pieces = para.split(/(?<=[.!?…:;])\\s+/);\n for (const piece of pieces) {\n if (!piece) continue;\n const span = document.createElement('span');\n span.className = 'ink-in';\n span.style.animationDelay = `${Math.min(i * 40, 1200)}ms`;\n span.textContent = `${piece} `;\n el.appendChild(span);\n i++;\n }\n }\n}\n\n// Ink-drop ripple from the click point. The button needs position:relative\n// and overflow:hidden (the .go class provides both).\nexport function attachInkRipple(btn) {\n if (!btn) return;\n btn.addEventListener('click', (e) => {\n const r = btn.getBoundingClientRect();\n const drop = document.createElement('span');\n drop.className = 'ripple';\n drop.style.left = `${e.clientX - r.left || r.width / 2}px`;\n drop.style.top = `${e.clientY - r.top || r.height / 2}px`;\n btn.appendChild(drop);\n drop.addEventListener('animationend', () => drop.remove());\n });\n}\n","// Mặc Hi app orchestration: boot → engine init with progress → translate\n// (typed text + .txt files with encoding sniffing). All engine work happens\n// on this thread via async WebGPU submits — the UI stays responsive.\nimport { renderApp } from './ui.js';\nimport { createEngine } from './engine.js';\nimport { MODELS, modelById, DEFAULT_MODEL_ID } from './models.js';\nimport { decodeBytes, ENCODING_CHOICES } from './encoding.js';\nimport { startPetals, inkRevealText, attachInkRipple } from './fx.js';\nimport './app.css';\n\nconst MAX_FILE_BYTES = 20 * 1024 * 1024;\nconst AUTO_MAX_CHARS = 600;\nconst AUTO_DEBOUNCE_MS = 900;\nconst AUTO_KEY = 'moxhi.autotranslate';\nconst MODEL_KEY = 'moxhi.model';\n\nconst ENC_LABELS = {\n auto: 'Tự động', 'utf-8': 'UTF-8', gb18030: 'GB18030 / GBK', big5: 'Big5',\n 'utf-16le': 'UTF-16 LE', 'utf-16be': 'UTF-16 BE',\n};\n\nconst reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;\nconst refs = renderApp(document.querySelector('#app'));\nstartPetals(refs.petals, { reduced });\nattachInkRipple(refs.goBtn);\nattachInkRipple(refs.fileGoBtn);\n\nconst state = {\n engine: null,\n busy: false,\n queued: false,\n stopFlag: false,\n file: null, // { name, bytes, text, encoding }\n fileResultText: null,\n filePartial: null, // row texts of an interrupted file run — resume fuel\n};\n\n// Windows idle-sleeps mid-translation (GPU compute doesn't count as user\n// activity — confirmed in the event log) and takes the WebGPU device with\n// it. Hold a screen wake lock while translating.\nlet wakeLock = null;\nasync function acquireWake() {\n try { wakeLock = (await navigator.wakeLock?.request('screen')) ?? null; } catch { wakeLock = null; }\n}\nfunction releaseWake() {\n wakeLock?.release().catch(() => {});\n wakeLock = null;\n}\ndocument.addEventListener('visibilitychange', () => {\n // The browser auto-releases the lock when the tab hides; re-arm on return.\n if (document.visibilityState === 'visible' && state.busy && !wakeLock) acquireWake();\n});\n\nconst show = (el, on = true) => { el.hidden = !on; };\nconst fmtMB = (b) => (b / 1048576).toFixed(1);\nconst fmtDur = (ms) => (ms < 950 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`);\n\nfunction showError(msg, retry) {\n refs.errMsg.textContent = msg;\n show(refs.errBanner);\n refs.retryBtn.onclick = () => { show(refs.errBanner, false); retry(); };\n}\n\n// --- boot -----------------------------------------------------------------\n\nconst PHASE_LABELS = {\n gpu: 'Gọi linh khí GPU…',\n weights: 'Tải trọng số…',\n tokenizer: 'Nạp từ điển…',\n autotune: 'Đo mạch thiết bị (chỉ lần đầu)…',\n};\n\nasync function init() {\n show(refs.gate, false);\n show(refs.studio, false);\n show(refs.loading, true);\n const model = modelById(localStorage.getItem(MODEL_KEY) ?? DEFAULT_MODEL_ID);\n refs.modelSelect.value = model.id;\n refs.modelSelect.disabled = true;\n refs.loadNote.innerHTML = `Lần đầu tải ~${model.downloadMB} MB trọng số (${model.name}) — `\n + 'các lần sau mở nhanh nhờ bộ nhớ đệm của trình duyệt.';\n try {\n state.engine = await createEngine({\n model,\n onPhase: (phase) => {\n if (phase === 'ready') return;\n refs.phaseLabel.textContent = PHASE_LABELS[phase] ?? phase;\n if (phase === 'tokenizer') refs.brushFill.style.width = '100%';\n },\n onProgress: (loaded, total) => {\n const pct = total ? Math.round((loaded / total) * 100) : 0;\n refs.brushFill.style.width = `${pct}%`;\n refs.phaseLabel.textContent =\n `Tải trọng số — ${fmtMB(loaded)} / ${fmtMB(total)} MB (${pct}%)`;\n },\n onDeviceLost: (lost) => {\n // Mid-translation the translate loop throws and its catch owns the\n // banner (with resume). This handles loss while idle.\n if (state.busy) return;\n showError(`GPU bị ngắt kết nối (${lost.reason}): ${lost.message || 'không rõ nguyên nhân'}. `\n + 'Bấm thử lại để khởi động lại bộ dịch.', init);\n },\n });\n } catch (err) {\n console.error(err);\n refs.modelSelect.disabled = false;\n const msg = String(err?.message ?? err);\n if (/WebGPU unavailable|no adapter/i.test(msg)) {\n show(refs.loading, false);\n refs.gateErr.textContent = `Chi tiết: ${msg}`;\n show(refs.gate, true);\n } else {\n showError(`Không khởi động được bộ dịch: ${msg}`, init);\n }\n return;\n }\n refs.modelSelect.disabled = false;\n const { gpuName, dtype, lmHeadQ8, tunedFrom, modelName } = state.engine.info;\n const tuneNote = tunedFrom === 'cache' ? 'autotune (đệm)'\n : tunedFrom === 'measured' ? 'autotune'\n : tunedFrom === 'partial' ? 'autotune (một phần)' : 'routing mặc định';\n const build = typeof __BUILD__ !== 'undefined' ? __BUILD__ : 'dev';\n refs.statGpu.textContent =\n `${modelName} · ${gpuName} · ${dtype}${lmHeadQ8 ? ' + lm_head int8' : ''} · ${tuneNote} · ${build}`;\n // Hybrid-laptop trap: browsers often hand WebGPU the integrated GPU even\n // with the high-performance hint. If we landed on an Intel iGPU, tell the\n // user how to unlock the discrete one (~30× on a typical dGPU laptop).\n if (/\\b(intel|iris|uhd|xe)\\b/i.test(gpuName)) {\n const tip = document.createElement('div');\n tip.className = 'gpu-tip';\n tip.textContent = 'Đang chạy trên GPU tích hợp. Máy có GPU rời? '\n + 'Windows: Settings → Display → Graphics → thêm trình duyệt → High performance, '\n + 'rồi mở lại trình duyệt — tốc độ có thể nhanh hơn hàng chục lần.';\n refs.statGpu.parentElement.appendChild(tip);\n }\n show(refs.loading, false);\n show(refs.studio, true);\n refs.src.focus();\n // E2E hook (used by the headless smoke test and handy in devtools).\n window.__moxhi = { engine: state.engine, build, translate: (t) => state.engine.translateText(t) };\n document.dispatchEvent(new CustomEvent('moxhi:ready'));\n}\n\n// --- typed-text translation -------------------------------------------------\n\nfunction setBusy(on) {\n state.busy = on;\n refs.goBtn.classList.toggle('busy', on);\n refs.goBtn.disabled = on;\n refs.fileGoBtn.disabled = on;\n refs.modelSelect.disabled = on;\n}\n\nasync function doTranslate() {\n if (!state.engine) return;\n if (state.busy) { state.queued = true; return; }\n const text = refs.src.value;\n if (!text.trim()) { refs.out.textContent = ''; refs.progress.textContent = ''; return; }\n setBusy(true);\n refs.progress.textContent = 'đang dịch…';\n try {\n const res = await state.engine.translateText(text, {\n onBatch: (i, n, translated) => {\n const done = translated.filter(Boolean).length;\n refs.progress.textContent = `câu ${done}/${translated.length}`;\n if (n > 1) refs.out.textContent = translated.filter(Boolean).join(' ');\n },\n });\n inkRevealText(refs.out, res.text, { reduced });\n refs.progress.textContent = '';\n refs.statPerf.textContent =\n `${Math.round(res.stats.tokPerSec)} tok/s · ${fmtDur(res.stats.ms)} · ${res.stats.sentences} câu`\n + (res.stats.t2s ? ' · phồn→giản' : '');\n } catch (err) {\n console.error(err);\n refs.progress.textContent = '';\n if (state.engine?.isLost?.()) {\n const { reason, message } = state.engine.isLost();\n showError(`GPU bị ngắt kết nối (${reason}): ${message || 'không rõ nguyên nhân'}. `\n + 'Thử lại sẽ khởi động lại bộ dịch.',\n async () => { await init(); doTranslate(); });\n } else {\n showError(`Dịch lỗi: ${err?.message ?? err}`, doTranslate);\n }\n } finally {\n setBusy(false);\n if (state.queued) { state.queued = false; doTranslate(); }\n }\n}\n\nrefs.goBtn.addEventListener('click', doTranslate);\nrefs.src.addEventListener('keydown', (e) => {\n if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); doTranslate(); }\n});\n\n// Auto-translate on idle, short texts only.\nrefs.autoTgl.checked = (localStorage.getItem(AUTO_KEY) ?? '1') === '1';\nrefs.autoTgl.addEventListener('change', () => {\n localStorage.setItem(AUTO_KEY, refs.autoTgl.checked ? '1' : '0');\n});\n\n// Model picker: switching tears the engine down and re-inits with the new\n// weights (dims come from the new manifest; autotune re-measures once per\n// device+model). A partial file translation from another model is not\n// resume fuel — rows would mix two models' outputs.\nrefs.modelSelect.replaceChildren(...MODELS.map((m) => {\n const o = document.createElement('option');\n o.value = m.id;\n o.textContent = m.name;\n return o;\n}));\nrefs.modelSelect.value = modelById(localStorage.getItem(MODEL_KEY) ?? DEFAULT_MODEL_ID).id;\nrefs.modelSelect.addEventListener('change', () => {\n localStorage.setItem(MODEL_KEY, refs.modelSelect.value);\n state.filePartial = null;\n init();\n});\nlet autoTimer = 0;\nrefs.src.addEventListener('input', () => {\n refs.srcCount.textContent = `${refs.src.value.length} ký tự`;\n clearTimeout(autoTimer);\n if (!refs.autoTgl.checked || !state.engine) return;\n if (refs.src.value.length > AUTO_MAX_CHARS) {\n refs.progress.textContent = 'văn bản dài — bấm Dịch';\n return;\n }\n refs.progress.textContent = '';\n autoTimer = setTimeout(doTranslate, AUTO_DEBOUNCE_MS);\n});\n\nrefs.pasteBtn.addEventListener('click', async () => {\n try {\n refs.src.value = await navigator.clipboard.readText();\n refs.src.dispatchEvent(new Event('input'));\n } catch {\n refs.src.focus();\n refs.progress.textContent = 'trình duyệt chặn đọc clipboard — dán bằng Ctrl+V';\n }\n});\nrefs.clearBtn.addEventListener('click', () => {\n refs.src.value = '';\n refs.out.textContent = '';\n refs.src.dispatchEvent(new Event('input'));\n refs.src.focus();\n});\nrefs.copyBtn.addEventListener('click', async () => {\n const t = refs.out.textContent;\n if (t) await navigator.clipboard.writeText(t).catch(() => {});\n});\n\nfunction downloadText(name, text) {\n const a = document.createElement('a');\n a.href = URL.createObjectURL(new Blob([text], { type: 'text/plain;charset=utf-8' }));\n a.download = name;\n a.click();\n URL.revokeObjectURL(a.href);\n}\nrefs.saveBtn.addEventListener('click', () => {\n const t = refs.out.textContent;\n if (t) downloadText('mac-hi.txt', t);\n});\n\n// --- .txt file mode ---------------------------------------------------------\n\nfunction renderEncSelect(detected) {\n refs.encSelect.replaceChildren(...ENCODING_CHOICES.map((v) => {\n const o = document.createElement('option');\n o.value = v;\n o.textContent = v === 'auto' ? `${ENC_LABELS.auto} (${ENC_LABELS[detected] ?? detected})` : ENC_LABELS[v];\n return o;\n }));\n refs.encSelect.value = 'auto';\n}\n\nfunction refreshFilePreview() {\n refs.filePreview.textContent = state.file.text.slice(0, 500)\n + (state.file.text.length > 500 ? ' …' : '');\n}\n\nasync function handleFile(f) {\n if (!f) return;\n if (f.size > MAX_FILE_BYTES) {\n showError(`File ${fmtMB(f.size)} MB vượt giới hạn 20 MB.`, () => {});\n return;\n }\n const bytes = new Uint8Array(await f.arrayBuffer());\n const { text, encoding } = decodeBytes(bytes, 'auto');\n state.file = { name: f.name, bytes, text, encoding };\n state.fileResultText = null;\n state.filePartial = null;\n refs.fileName.textContent = f.name;\n refs.fileSize.textContent = `${fmtMB(f.size)} MB · ${encoding}`;\n renderEncSelect(encoding);\n refreshFilePreview();\n show(refs.drop, false);\n show(refs.filecard, true);\n show(refs.fileSaveBtn, false);\n show(refs.fileResult, false);\n refs.fileProgress.textContent = '';\n}\n\nrefs.browseBtn.addEventListener('click', () => refs.fileInput.click());\nrefs.fileInput.addEventListener('change', () => handleFile(refs.fileInput.files[0]));\nfor (const [ev, on] of [['dragover', true], ['dragleave', false]]) {\n refs.drop.addEventListener(ev, (e) => {\n e.preventDefault();\n refs.drop.classList.toggle('dragover', on);\n });\n}\nrefs.drop.addEventListener('drop', (e) => {\n e.preventDefault();\n refs.drop.classList.remove('dragover');\n handleFile(e.dataTransfer.files[0]);\n});\n\nrefs.encSelect.addEventListener('change', () => {\n const choice = refs.encSelect.value;\n const { text, encoding } = decodeBytes(state.file.bytes, choice);\n state.file.text = text;\n state.file.encoding = encoding;\n state.filePartial = null; // different decode ⇒ different rows — stale fuel\n refs.fileSize.textContent = `${fmtMB(state.file.bytes.length)} MB · ${encoding}`;\n refreshFilePreview();\n});\n\nrefs.fileDropBtn.addEventListener('click', () => {\n state.file = null;\n state.fileResultText = null;\n state.filePartial = null;\n refs.fileInput.value = '';\n show(refs.filecard, false);\n show(refs.drop, true);\n});\n\nasync function doTranslateFile() {\n if (!state.engine || !state.file || state.busy) return;\n setBusy(true);\n state.stopFlag = false;\n show(refs.fileCancelBtn, true);\n show(refs.fileSaveBtn, false);\n show(refs.fileResult, false);\n acquireWake();\n const t0 = performance.now();\n try {\n const res = await state.engine.translateText(state.file.text, {\n pack: true, // file mode: merge same-paragraph sentences into ~224-char rows\n resume: state.filePartial, // skip batches finished before a crash/cancel\n shouldStop: () => state.stopFlag,\n onBatch: (i, n, translated) => {\n state.filePartial = translated;\n const done = translated.filter(Boolean).length;\n const pct = Math.round((i / n) * 100);\n const elapsed = (performance.now() - t0) / 1000;\n const eta = i > 0 ? Math.max(0, (elapsed / i) * (n - i)) : 0;\n refs.fileProgress.textContent =\n `${done}/${translated.length} cụm câu · ${pct}% · còn ~${Math.ceil(eta)}s`;\n },\n });\n if (!res.cancelled) state.filePartial = null;\n state.fileResultText = res.text + (res.cancelled ? '\\n\\n[— bản dịch dở dang —]' : '');\n refs.fileProgress.textContent = res.cancelled\n ? 'đã hủy — phần dịch dở vẫn tải được'\n : `xong — ${fmtDur(res.stats.ms)} · ${Math.round(res.stats.tokPerSec)} tok/s`\n + (res.stats.forcedEos ? ` · ⚠ ${res.stats.forcedEos} cụm chạm trần token` : '');\n refs.fileResult.textContent = state.fileResultText.slice(0, 1000)\n + (state.fileResultText.length > 1000 ? ' …' : '');\n show(refs.fileResult, true);\n show(refs.fileSaveBtn, true);\n refs.statPerf.textContent =\n `${Math.round(res.stats.tokPerSec)} tok/s · ${fmtDur(res.stats.ms)} · ${res.stats.sentences} cụm câu`\n + (res.stats.t2s ? ' · phồn→giản' : '');\n } catch (err) {\n console.error(err);\n const done = state.filePartial ? state.filePartial.filter(Boolean).length : 0;\n if (state.engine?.isLost?.()) {\n const { reason, message } = state.engine.isLost();\n refs.fileProgress.textContent = done ? `đã dịch ${done} cụm — sẽ tiếp tục từ đó` : '';\n showError(`GPU bị ngắt kết nối (${reason}): ${message || 'không rõ nguyên nhân'}. `\n + 'Thử lại sẽ khởi động lại bộ dịch và dịch tiếp từ chỗ dừng.',\n async () => { await init(); doTranslateFile(); });\n } else {\n refs.fileProgress.textContent = '';\n showError(`Dịch file lỗi: ${err?.message ?? err}`, doTranslateFile);\n }\n } finally {\n releaseWake();\n show(refs.fileCancelBtn, false);\n setBusy(false);\n }\n}\n\nrefs.fileGoBtn.addEventListener('click', doTranslateFile);\nrefs.fileCancelBtn.addEventListener('click', () => { state.stopFlag = true; });\nrefs.fileSaveBtn.addEventListener('click', () => {\n if (!state.fileResultText) return;\n const base = state.file.name.replace(/\\.txt$/i, '');\n downloadText(`${base}.vi.txt`, state.fileResultText);\n});\n\ninit();\n\n// PWA: sw.js only exists in deployed builds (deploy_pages.mjs generates it\n// from scripts/sw_template.js) — dev stays SW-free so HMR isn't fought over.\nif (import.meta.env.PROD && 'serviceWorker' in navigator) {\n window.addEventListener('load', () => {\n navigator.serviceWorker.register('/sw.js').catch(() => { /* app works without offline */ });\n });\n}\n"],"names":["heroUrl","SEAL_FONT","TEMPLATE","renderApp","root","refs","el","ENCODING_CHOICES","COMMON_HANZI","hasBOM","b","sig","v","scoreCJK","text","cjk","common","bad","ch","c","sniffEncoding","bytes","sample","gb","b5","sGb","sB5","decodeBytes","encoding","enc","startPetals","canvas","reduced","ctx","dpr","sprite","s","g","W","H","petals","count","spawn","top","resize","last","raf","tick","now","dt","i","p","inkRevealText","para","pieces","piece","span","attachInkRipple","btn","e","r","drop","MAX_FILE_BYTES","AUTO_MAX_CHARS","AUTO_DEBOUNCE_MS","AUTO_KEY","MODEL_KEY","ENC_LABELS","state","wakeLock","acquireWake","releaseWake","show","on","fmtMB","fmtDur","ms","showError","msg","retry","PHASE_LABELS","init","model","modelById","DEFAULT_MODEL_ID","createEngine","phase","loaded","total","pct","lost","err","gpuName","dtype","lmHeadQ8","tunedFrom","modelName","tuneNote","build","tip","t","setBusy","doTranslate","res","n","translated","done","reason","message","MODELS","m","o","autoTimer","downloadText","name","renderEncSelect","detected","refreshFilePreview","handleFile","f","ev","choice","doTranslateFile","t0","elapsed","eta","base"],"mappings":"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"}
|
icon-192.png
ADDED
|
|
icon-512.png
ADDED
|
|
icon-maskable-512.png
ADDED
|
|
index.html
CHANGED
|
@@ -8,8 +8,11 @@
|
|
| 8 |
<meta property="og:description" content="Máy dịch chạy 100% trên GPU máy bạn. Không máy chủ, không gửi dữ liệu." />
|
| 9 |
<meta property="og:image" content="https://moxhi.pages.dev/og.jpg" />
|
| 10 |
<title>Mặc Hi 墨曦 — dịch truyện Trung–Việt trong trình duyệt</title>
|
|
|
|
|
|
|
|
|
|
| 11 |
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='10' fill='%23b3372f'/%3E%3Ctext x='32' y='30' font-size='26' text-anchor='middle' fill='%23f4eee3' font-family='serif'%3E墨%3C/text%3E%3Ctext x='32' y='56' font-size='26' text-anchor='middle' fill='%23f4eee3' font-family='serif'%3E曦%3C/text%3E%3C/svg%3E" />
|
| 12 |
-
<script type="module" crossorigin src="/assets/main-
|
| 13 |
<link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-B5Qt9EMX.js">
|
| 14 |
<link rel="modulepreload" crossorigin href="/assets/device-Z_ibNtmX.js">
|
| 15 |
<link rel="modulepreload" crossorigin href="/assets/constants-C8VbFpic.js">
|
|
|
|
| 8 |
<meta property="og:description" content="Máy dịch chạy 100% trên GPU máy bạn. Không máy chủ, không gửi dữ liệu." />
|
| 9 |
<meta property="og:image" content="https://moxhi.pages.dev/og.jpg" />
|
| 10 |
<title>Mặc Hi 墨曦 — dịch truyện Trung–Việt trong trình duyệt</title>
|
| 11 |
+
<link rel="manifest" href="/manifest.webmanifest" />
|
| 12 |
+
<meta name="theme-color" content="#b3372f" />
|
| 13 |
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
| 14 |
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='10' fill='%23b3372f'/%3E%3Ctext x='32' y='30' font-size='26' text-anchor='middle' fill='%23f4eee3' font-family='serif'%3E墨%3C/text%3E%3Ctext x='32' y='56' font-size='26' text-anchor='middle' fill='%23f4eee3' font-family='serif'%3E曦%3C/text%3E%3C/svg%3E" />
|
| 15 |
+
<script type="module" crossorigin src="/assets/main-CS5AEj4z.js"></script>
|
| 16 |
<link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-B5Qt9EMX.js">
|
| 17 |
<link rel="modulepreload" crossorigin href="/assets/device-Z_ibNtmX.js">
|
| 18 |
<link rel="modulepreload" crossorigin href="/assets/constants-C8VbFpic.js">
|
manifest.webmanifest
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Mặc Hi — dịch truyện Trung → Việt",
|
| 3 |
+
"short_name": "Mặc Hi",
|
| 4 |
+
"description": "Dịch truyện Trung → Việt ngay trên máy bằng WebGPU — không gửi văn bản đi đâu, dùng được offline sau lần tải model đầu tiên.",
|
| 5 |
+
"lang": "vi",
|
| 6 |
+
"start_url": "/",
|
| 7 |
+
"scope": "/",
|
| 8 |
+
"display": "standalone",
|
| 9 |
+
"orientation": "portrait",
|
| 10 |
+
"background_color": "#f4eee3",
|
| 11 |
+
"theme_color": "#b3372f",
|
| 12 |
+
"icons": [
|
| 13 |
+
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
| 14 |
+
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" },
|
| 15 |
+
{ "src": "/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
|
| 16 |
+
]
|
| 17 |
+
}
|
sw.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Service worker TEMPLATE — deploy_pages.mjs stamps 0454a43 and
|
| 2 |
+
// ["/","/manifest.webmanifest","/icon-192.png","/assets/main-CS5AEj4z.js","/assets/modulepreload-polyfill-B5Qt9EMX.js","/assets/device-Z_ibNtmX.js","/assets/constants-C8VbFpic.js","/assets/shapes-BCQ9D81L.js","/assets/f16-wKKZr58e.js","/assets/weights-B-okS09o.js","/assets/tokenizer-Ex1u7wXD.js","/assets/pipelines-DD24nEJg.js","/assets/encoder-DX8MOfcH.js","/assets/decoder-DI6VDxxO.js","/assets/generate-C4ruG1zm.js","/assets/autotune-DBJGFXNj.js","/assets/engine-BTyy_WlH.js","/assets/main-DNpzOJCp.css","/assets/playfair-vietnamese-Cabi7G8-.woff2","/assets/playfair-latin-BOwq7MWX.woff2"] and writes the result to dist/sw.js. Never served in dev.
|
| 3 |
+
//
|
| 4 |
+
// Cache layers (all same-origin GET only):
|
| 5 |
+
// moxhi-shell-<build> app shell: precached index + hashed /assets/*
|
| 6 |
+
// (cache-first — names are content-hashed), replaced
|
| 7 |
+
// wholesale on each deploy.
|
| 8 |
+
// moxhi-data-v1 small mutable files (/model/*.json, weights
|
| 9 |
+
// manifest.json, manifest.webmanifest, icons):
|
| 10 |
+
// network-first, cache is the offline fallback.
|
| 11 |
+
// weights shards (.bin) are NOT touched here — src/engine/weights.js owns
|
| 12 |
+
// them in its own Cache Storage bucket (content-addressed names).
|
| 13 |
+
/* global self, caches, fetch, location */
|
| 14 |
+
|
| 15 |
+
const SHELL = 'moxhi-shell-0454a43';
|
| 16 |
+
const DATA = 'moxhi-data-v1';
|
| 17 |
+
const PRECACHE = ["/","/manifest.webmanifest","/icon-192.png","/assets/main-CS5AEj4z.js","/assets/modulepreload-polyfill-B5Qt9EMX.js","/assets/device-Z_ibNtmX.js","/assets/constants-C8VbFpic.js","/assets/shapes-BCQ9D81L.js","/assets/f16-wKKZr58e.js","/assets/weights-B-okS09o.js","/assets/tokenizer-Ex1u7wXD.js","/assets/pipelines-DD24nEJg.js","/assets/encoder-DX8MOfcH.js","/assets/decoder-DI6VDxxO.js","/assets/generate-C4ruG1zm.js","/assets/autotune-DBJGFXNj.js","/assets/engine-BTyy_WlH.js","/assets/main-DNpzOJCp.css","/assets/playfair-vietnamese-Cabi7G8-.woff2","/assets/playfair-latin-BOwq7MWX.woff2"];
|
| 18 |
+
// Model/data files the app fetches during startup — often BEFORE this SW has
|
| 19 |
+
// claimed the page on its very first visit, so runtime caching would miss
|
| 20 |
+
// them and the first offline reload would fail engine init. Precache them
|
| 21 |
+
// here (best-effort; /model/* is immutable-cached so this is usually free).
|
| 22 |
+
const DATA_PRECACHE = ["/weights/manifest.json","/model/moxhi/tokenizer.json","/model/moxhi/tokenizer_config.json","/weights/hachimi60/manifest.json","/model/hachimi60/tokenizer.json","/model/hachimi60/tokenizer_config.json"];
|
| 23 |
+
|
| 24 |
+
self.addEventListener('install', (event) => {
|
| 25 |
+
event.waitUntil((async () => {
|
| 26 |
+
await (await caches.open(SHELL)).addAll(PRECACHE);
|
| 27 |
+
const data = await caches.open(DATA);
|
| 28 |
+
await Promise.allSettled(DATA_PRECACHE.map(async (url) => {
|
| 29 |
+
const res = await fetch(url);
|
| 30 |
+
if (res.ok) await data.put(url, res);
|
| 31 |
+
}));
|
| 32 |
+
})());
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
self.addEventListener('activate', (event) => {
|
| 36 |
+
event.waitUntil((async () => {
|
| 37 |
+
for (const key of await caches.keys()) {
|
| 38 |
+
if (key.startsWith('moxhi-shell-') && key !== SHELL) await caches.delete(key);
|
| 39 |
+
}
|
| 40 |
+
await self.clients.claim();
|
| 41 |
+
})());
|
| 42 |
+
});
|
| 43 |
+
|
| 44 |
+
// ignoreVary everywhere: entries are keyed by content-hashed (or
|
| 45 |
+
// version-checked) same-origin URLs, but servers may stamp `Vary: Origin`
|
| 46 |
+
// (vite preview does) — and module/CSS requests carry an Origin header while
|
| 47 |
+
// install-time fetches don't, so honoring Vary would miss every hit offline.
|
| 48 |
+
const MATCH_OPTS = { ignoreVary: true };
|
| 49 |
+
|
| 50 |
+
async function cacheFirst(req) {
|
| 51 |
+
const cache = await caches.open(SHELL);
|
| 52 |
+
const hit = await cache.match(req, MATCH_OPTS);
|
| 53 |
+
if (hit) return hit;
|
| 54 |
+
const res = await fetch(req);
|
| 55 |
+
if (res.ok) await cache.put(req, res.clone());
|
| 56 |
+
return res;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
async function networkFirst(req, cacheName, fallbackUrl) {
|
| 60 |
+
const cache = await caches.open(cacheName);
|
| 61 |
+
try {
|
| 62 |
+
const res = await fetch(req);
|
| 63 |
+
if (res.ok) await cache.put(req, res.clone());
|
| 64 |
+
return res;
|
| 65 |
+
} catch (err) {
|
| 66 |
+
const hit = (await cache.match(req, MATCH_OPTS))
|
| 67 |
+
?? (fallbackUrl && await cache.match(fallbackUrl, MATCH_OPTS));
|
| 68 |
+
if (hit) return hit;
|
| 69 |
+
throw err;
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
self.addEventListener('fetch', (event) => {
|
| 74 |
+
const req = event.request;
|
| 75 |
+
if (req.method !== 'GET') return;
|
| 76 |
+
const url = new URL(req.url);
|
| 77 |
+
if (url.origin !== location.origin) return;
|
| 78 |
+
if (url.pathname.endsWith('.bin')) return; // weights loader's own cache
|
| 79 |
+
|
| 80 |
+
if (req.mode === 'navigate') {
|
| 81 |
+
event.respondWith(networkFirst(req, SHELL, '/'));
|
| 82 |
+
} else if (url.pathname.startsWith('/assets/')) {
|
| 83 |
+
event.respondWith(cacheFirst(req));
|
| 84 |
+
} else {
|
| 85 |
+
event.respondWith(networkFirst(req, DATA));
|
| 86 |
+
}
|
| 87 |
+
});
|