v6: modified Kneser-Ney + per-word extension rule (held-out keystrokes saved 22.2% -> 23.0%, precision 90.2% -> 90.8%)
Browse files- README.md +17 -10
- demo.js +3 -3
- ghost-typer.js +2 -2
- model-f32.bin +2 -2
- model.bin +2 -2
- src/autocomplete.ts +10 -3
README.md
CHANGED
|
@@ -33,13 +33,18 @@ zero-dependency TypeScript runtime. **Demo:** https://tmickleydoyle.github.io/gh
|
|
| 33 |
|
| 34 |
| File | Precision | Size | Held-out precision | Keystrokes saved | Use it for |
|
| 35 |
|---|---|---|---|---|---|
|
| 36 |
-
| **`model.bin`** | **uint8** logit changes, one scale per context length | **10.7 MB** | 90.
|
| 37 |
-
| `model-f32.bin` | float32 (unquantized) | 16.8 MB | 90.
|
| 38 |
|
| 39 |
-
Both files hold the same compile: 30,893 words and 512,533 contexts. Quantization
|
| 40 |
-
|
| 41 |
-
measured by typing 1,711 held-out prompts one character at a time. The runtime reads
|
| 42 |
-
file.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
The runtime itself is `ghost-typer.js` (bundled ESM, ~5 KB) with sources in `src/`. A demo page
|
| 45 |
(`index.html`, `demo.js`) loads `./model.bin`.
|
|
@@ -61,10 +66,12 @@ input.addEventListener("input", () => {
|
|
| 61 |
});
|
| 62 |
```
|
| 63 |
|
| 64 |
-
|
| 65 |
-
|
|
|
|
|
|
|
| 66 |
|
| 67 |
-
Per keystroke it takes 2.
|
| 68 |
360 ms in Chrome.
|
| 69 |
|
| 70 |
To try the demo locally: `git clone https://huggingface.co/tmickleydoyle/ghost-typer-web`, then
|
|
@@ -75,7 +82,7 @@ To try the demo locally: `git clone https://huggingface.co/tmickleydoyle/ghost-t
|
|
| 75 |
The [main repo](https://huggingface.co/tmickleydoyle/ghost-typer) holds the float32
|
| 76 |
`LlamaForCausalLM` (`model.safetensors`, 400M parameters, 97% zeros) and the compiler. Both
|
| 77 |
artifacts are compiled from the same text by the same closed-form rules: an interpolated
|
| 78 |
-
Kneser-Ney 5-gram, with one MLP neuron per context.
|
| 79 |
- **What's stored:** the browser model keeps only the non-zero parts, namely each neuron's
|
| 80 |
context tokens and the logit changes it writes.
|
| 81 |
- **Different settings:** it's compiled with browser settings (30,893-word vocabulary vs
|
|
|
|
| 33 |
|
| 34 |
| File | Precision | Size | Held-out precision | Keystrokes saved | Use it for |
|
| 35 |
|---|---|---|---|---|---|
|
| 36 |
+
| **`model.bin`** | **uint8** logit changes, one scale per context length | **10.7 MB** | 90.8% | 23.0% | **apps (recommended)** |
|
| 37 |
+
| `model-f32.bin` | float32 (unquantized) | 16.8 MB | 90.7% | 23.1% | reference, exactness checks |
|
| 38 |
|
| 39 |
+
Both files hold the same compile: 30,893 words and 512,533 contexts. Quantization costs 0.14
|
| 40 |
+
points of keystrokes saved (23.00% vs 23.14%) and leaves precision unchanged (90.79% vs
|
| 41 |
+
90.73%), measured by typing 1,711 held-out prompts one character at a time. The runtime reads
|
| 42 |
+
either file.
|
| 43 |
+
|
| 44 |
+
**Update (v6):** modified Kneser-Ney smoothing and a per-word extension rule. Previous build:
|
| 45 |
+
90.2% precision, 22.2% keystrokes saved, same size. `model.bin` now carries `policy.ext`, which
|
| 46 |
+
only the current runtime (`ghost-typer.js`, `src/`) reads. Update the runtime together with the
|
| 47 |
+
model: an older runtime ignores `ext` and its suggestions run below 90% precision.
|
| 48 |
|
| 49 |
The runtime itself is `ghost-typer.js` (bundled ESM, ~5 KB) with sources in `src/`. A demo page
|
| 50 |
(`index.html`, `demo.js`) loads `./model.bin`.
|
|
|
|
| 66 |
});
|
| 67 |
```
|
| 68 |
|
| 69 |
+
The default policy shows the word being typed if it is ≥ 62% likely (τ = 0.62), then extends
|
| 70 |
+
word by word while each next word is ≥ 80% likely on its own (`ext` = 0.8).
|
| 71 |
+
`ac.suggest(text, { ...ac.policy, tau: 0.9 })` gives a stricter threshold (96.7% precision,
|
| 72 |
+
15.1% keystrokes saved).
|
| 73 |
|
| 74 |
+
Per keystroke it takes 2.5 ms median and 4.3 ms p95 (Bun). Loading the uint8 file takes about
|
| 75 |
360 ms in Chrome.
|
| 76 |
|
| 77 |
To try the demo locally: `git clone https://huggingface.co/tmickleydoyle/ghost-typer-web`, then
|
|
|
|
| 82 |
The [main repo](https://huggingface.co/tmickleydoyle/ghost-typer) holds the float32
|
| 83 |
`LlamaForCausalLM` (`model.safetensors`, 400M parameters, 97% zeros) and the compiler. Both
|
| 84 |
artifacts are compiled from the same text by the same closed-form rules: an interpolated
|
| 85 |
+
modified Kneser-Ney 5-gram, with one MLP neuron per context.
|
| 86 |
- **What's stored:** the browser model keeps only the non-zero parts, namely each neuron's
|
| 87 |
context tokens and the logit changes it writes.
|
| 88 |
- **Different settings:** it's compiled with browser settings (30,893-word vocabulary vs
|
demo.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
-
function f(){let H=[];for(let
|
| 2 |
-
`),this.text=this.vocab.map((
|
| 3 |
-
`):[],
|
|
|
|
| 1 |
+
function f(){let H=[];for(let X=33;X<=126;X++)H.push(X);for(let X=161;X<=172;X++)H.push(X);for(let X=174;X<=255;X++)H.push(X);let J=[...H],Z=0;for(let X=0;X<256;X++)if(!H.includes(X))H.push(X),J.push(256+Z++);return new Map(H.map((X,K)=>[X,String.fromCodePoint(J[K])]))}var k=f(),y=new Map([...k].map(([H,J])=>[J,H])),h=/'s|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+/gu,u=new TextEncoder,b=new TextDecoder;function C(H){let J=H.normalize("NFKC").toLowerCase();if(J&&!/^\s/.test(J))J=" "+J;return(J.match(h)??[]).map((Z)=>[...u.encode(Z)].map((X)=>k.get(X)).join(""))}function z(H){return b.decode(new Uint8Array([...H].map((J)=>y.get(J)??63)))}var m="GHST",w=2;function n(H){let J=H&32768?-1:1,Z=H>>10&31,X=H&1023;if(Z===0)return J*0.00006103515625*(X/1024);if(Z===31)return X?NaN:J*(1/0);return J*2**(Z-15)*(1+X/1024)}class W{vocab;nPrev;order;eosId;unkId;policy;bytes;contexts;text;pieceIndex;base;orders;oov;stopAfter;candCache=new Map;distCache=new Map;static async load(H,J){if(H instanceof ArrayBuffer)return new W(H);let Z=await fetch(H,{cache:"no-cache"});if(!Z.ok||!Z.body)throw Error(`failed to fetch ${H}: ${Z.status}`);let X=Number(Z.headers.get("content-length"))||0,K=Z.body.getReader(),F=[],q=0;for(;;){let{done:M,value:R}=await K.read();if(M)break;if(F.push(R),q+=R.length,X)J?.(q/X)}let Y=new Uint8Array(q),G=0;for(let M of F)Y.set(M,G),G+=M.length;return J?.(1),new W(Y.buffer)}constructor(H){let J=new DataView(H),Z=new TextDecoder;if(Z.decode(new Uint8Array(H,0,4))!==m)throw Error("not a ghost-typer model.bin");let X=J.getUint32(4,!0);if(X!==w)throw Error(`model.bin version ${X}, runtime expects ${w}`);let K=J.getUint32(8,!0),F=JSON.parse(Z.decode(new Uint8Array(H,12,K))),q=12+K+(8-(12+K)%8)%8,Y=new Map;for(let Q of F.sections)Y.set(Q.name,Q);let G=(Q)=>{let V=Y.get(Q);return V?new Uint8Array(H,q+V.offset,V.bytes):null},M=(Q,V)=>{let B=Y.get(Q);return new V(H,q+B.offset,B.bytes/V.BYTES_PER_ELEMENT)},R=F.id_bytes===2?Uint16Array:Uint32Array;this.vocab=Z.decode(G("vocab")).split(`
|
| 2 |
+
`),this.text=this.vocab.map((Q)=>z(Q)),this.pieceIndex=new Map(this.vocab.map((Q,V)=>[Q,V]));let _=G("oov_keys").length?Z.decode(G("oov_keys")).split(`
|
| 3 |
+
`):[],U=G("oov_vals");this.oov=new Map(_.map((Q,V)=>[Q,U[V]/255])),this.nPrev=F.n_prev,this.order=this.nPrev+1,this.eosId=F.eos_id,this.unkId=F.unk_id,this.policy=F.policy,this.stopAfter=new Set(F.stop_after),this.bytes=H.byteLength,this.base=M("base",Float32Array),this.orders=F.orders.map((Q)=>{let V=M(`count${Q.k}`,Uint16Array),B=new Uint32Array(Q.contexts+1);for(let D=0;D<Q.contexts;D++)B[D+1]=B[D]+V[D];let L=new Float32Array(Q.pairs);if(F.deltas==="u8"){let D=G(`dvals${Q.k}`);for(let O=0;O<Q.pairs;O++)L[O]=D[O]*Q.scale}else if(F.deltas==="f32")L.set(M(`dvals${Q.k}`,Float32Array));else{let D=M(`dvals${Q.k}`,Uint16Array);for(let O=0;O<Q.pairs;O++)L[O]=n(D[O])}return{k:Q.k,n:Q.contexts,ctx:M(`ctx${Q.k}`,R),offsets:B,dids:M(`dids${Q.k}`,R),dvals:L,support:Y.has(`support${Q.k}`)?M(`support${Q.k}`,Float32Array):null}}),this.contexts=this.orders.reduce((Q,V)=>Q+V.n,0)}window(H){let J=H.length-1,Z=[];for(let X=this.nPrev;X>=0;X--)Z.push(H[Math.max(J-X,0)]);return Z}find(H,J){let Z=0,X=H.n-1;while(Z<=X){let K=Z+X>>1,F=0;for(let q=0;q<H.k&&F===0;q++)F=H.ctx[K*H.k+q]-J[q];if(F===0)return K;if(F<0)Z=K+1;else X=K-1}return-1}dist(H){let J=this.window(H),Z=J.join(","),X=this.distCache.get(Z);if(X)return X;let K=Float64Array.from(this.base),F=0;for(let R of this.orders){let _=this.find(R,J.slice(-R.k));if(_<0)continue;for(let U=R.offsets[_];U<R.offsets[_+1];U++)K[R.dids[U]]+=R.dvals[U];F=R.support?R.support[_]:1/0}let q=-1/0;for(let R of K)if(R>q)q=R;let Y=0,G=new Float64Array(K.length);for(let R=0;R<K.length;R++)Y+=G[R]=Math.exp(K[R]-q);for(let R=0;R<G.length;R++)G[R]/=Y;if(this.distCache.size>2000)this.distCache.clear();let M={probs:G,support:F};return this.distCache.set(Z,M),M}candidates(H){let J=this.candCache.get(H);if(!J){let Z=[];for(let X=this.unkId+1;X<this.vocab.length;X++)if(this.vocab[X].startsWith(H))Z.push(X);if(J=Int32Array.from(Z),this.candCache.size>5000)this.candCache.clear();this.candCache.set(H,J)}return J}steps(H,J=this.policy.max_words){let Z=C(H);if(!Z.length)return[];let X=Z[Z.length-1],K=this.candidates(X);if(!K.length)return[];let F=[this.eosId,...Z.slice(0,-1).map((U)=>this.pieceIndex.get(U)??this.unkId)],{probs:q,support:Y}=this.dist(F),G=K[0],M=0;for(let U of K)if(M+=q[U],q[U]>q[G])G=U;let R=(1-(this.oov.get(X)??0))*(q[G]/M),_=[{text:this.text[G].slice(z(X).length),prob:R,support:Y}];F.push(G);while(!this.stopAfter.has(this.vocab[F[F.length-1]])&&_.length<J){({probs:q,support:Y}=this.dist(F));let U=0;for(let Q=1;Q<q.length;Q++)if(q[Q]>q[U])U=Q;if(U===this.eosId)break;R*=q[U],_.push({text:this.text[U],prob:R,support:Y}),F.push(U)}return _}static apply(H,J){let Z="",X=0,K=Math.min(H.length,J.max_words);for(let F=0;F<K;F++){let q=H[F];if(q.support<J.min_support)break;if(F&&J.ext!=null){if(q.prob/H[F-1].prob<J.ext)break}else if(q.prob<J.tau)break;Z+=q.text,X=q.prob}return{ghost:Z,prob:X}}suggest(H,J=this.policy){let Z=this.steps(H,J.max_words);return{...W.apply(Z,J),steps:Z}}}var $=(H)=>document.getElementById(H),N=$("prompt"),j=$("ghost"),g=$("state"),I=$("meta"),l=["write a python fun","can you explain the differ","how do i conv","i'm getting an err"],P=(H)=>H.replace(/[&<>]/g,(J)=>({"&":"&","<":"<",">":">"})[J]),v=(H)=>H.toLocaleString("en-US"),S=(H)=>new Promise((J)=>setTimeout(J,H));async function a(){let H=performance.now(),J=await W.load("model.bin",(U)=>I.textContent=`loading model ${Math.round(U*100)}%`),Z=Math.round(performance.now()-H);I.textContent=`${(J.bytes/1e6).toFixed(1)} MB · ${v(J.vocab.length)} words · ${v(J.contexts)} contexts · loaded in ${Z} ms`;let X=[["strict",0.9],["default",J.policy.tau],["loose",0.6]],K={...J.policy},F="",q=0,Y=0,G=!0,M=(U="")=>{let Q=N.value,V=performance.now(),B=J.suggest(Q,K),L=performance.now()-V;F=B.ghost;let D=U?Q.slice(0,Q.length-U.length):Q;if(j.innerHTML=P(D)+(U?`<span class="accept">${P(U)}</span>`:"")+"<span>"+P(F)+"</span>",N.style.height="auto",N.style.height=Math.max(N.scrollHeight,j.scrollHeight)+"px",!Q){g.textContent=!G&&q?`saved ${Y} of ${q+Y} keystrokes`:" ";return}let O=B.steps.findIndex((A)=>A.text),T=O<0?"":B.steps.slice(0,O+1).map((A)=>A.text).join(""),x=F?`${Math.round(B.prob*100)}% sure`:T?`quiet · "${T.trim()}" only ${Math.round(B.steps[O].prob*100)}%`:"quiet · unknown",E=!G&&q+Y?` │ saved ${Math.round(100*Y/(q+Y))}%`:"";g.textContent=`${L.toFixed(2)} ms │ ${x}${E}`},R=()=>{if(!G)return;G=!1,N.value="",M()};async function _(){await S(600);while(G)for(let U of l){for(let Q=1;Q<=U.length&&G;Q++)N.value=U.slice(0,Q),M(),await S(55+Math.random()*60);if(!G)return;if(await S(900),!G)return;if(F){let Q=F;N.value+=Q,M(Q),await S(1400)}if(!G)return;N.value="",M(),await S(500)}}for(let[U,Q]of X){let V=document.createElement("button");V.textContent=U,V.setAttribute("aria-pressed",String(Q===K.tau)),V.onclick=()=>{K.tau=Q;for(let B of $("modes").children)B.setAttribute("aria-pressed",String(B===V));M()},$("modes").appendChild(V)}N.addEventListener("pointerdown",R),N.addEventListener("keydown",(U)=>{if(G){if(R(),U.key==="Tab"||U.key==="Escape")U.preventDefault();return}if(U.key==="Tab"&&F){U.preventDefault();let Q=F;N.value+=Q,Y+=Q.length-1,q+=1,M(Q)}else if(U.key==="Escape")N.value="",M()}),N.addEventListener("beforeinput",(U)=>{if(!G&&U.inputType.startsWith("insert"))q+=(U.data??"").length||1}),N.addEventListener("input",()=>M()),N.disabled=!1,M(),_()}a().catch((H)=>I.textContent=String(H));
|
ghost-typer.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
-
function E(){let J=[];for(let H=33;H<=126;H++)J.push(H);for(let H=161;H<=172;H++)J.push(H);for(let H=174;H<=255;H++)J.push(H);let Q=[...J],X=0;for(let H=0;H<256;H++)if(!J.includes(H))J.push(H),Q.push(256+X++);return new Map(J.map((H,
|
| 2 |
`),this.text=this.vocab.map((Z)=>L(Z)),this.pieceIndex=new Map(this.vocab.map((Z,K)=>[Z,K]));let O=U("oov_keys").length?X.decode(U("oov_keys")).split(`
|
| 3 |
-
`):[],q=U("oov_vals");this.oov=new Map(O.map((Z,K)=>[Z,q[K]/255])),this.nPrev=
|
|
|
|
| 1 |
+
function E(){let J=[];for(let H=33;H<=126;H++)J.push(H);for(let H=161;H<=172;H++)J.push(H);for(let H=174;H<=255;H++)J.push(H);let Q=[...J],X=0;for(let H=0;H<256;H++)if(!J.includes(H))J.push(H),Q.push(256+X++);return new Map(J.map((H,M)=>[H,String.fromCodePoint(Q[M])]))}var W=E(),z=new Map([...W].map(([J,Q])=>[Q,J])),P=/'s|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+/gu,I=new TextEncoder,g=new TextDecoder;function S(J){let Q=J.normalize("NFKC").toLowerCase();if(Q&&!/^\s/.test(Q))Q=" "+Q;return(Q.match(P)??[]).map((X)=>[...I.encode(X)].map((H)=>W.get(H)).join(""))}function L(J){return g.decode(new Uint8Array([...J].map((Q)=>z.get(Q)??63)))}var T="GHST",A=2;function k(J){let Q=J&32768?-1:1,X=J>>10&31,H=J&1023;if(X===0)return Q*0.00006103515625*(H/1024);if(X===31)return H?NaN:Q*(1/0);return Q*2**(X-15)*(1+H/1024)}class _{vocab;nPrev;order;eosId;unkId;policy;bytes;contexts;text;pieceIndex;base;orders;oov;stopAfter;candCache=new Map;distCache=new Map;static async load(J,Q){if(J instanceof ArrayBuffer)return new _(J);let X=await fetch(J,{cache:"no-cache"});if(!X.ok||!X.body)throw Error(`failed to fetch ${J}: ${X.status}`);let H=Number(X.headers.get("content-length"))||0,M=X.body.getReader(),$=[],D=0;for(;;){let{done:R,value:F}=await M.read();if(R)break;if($.push(F),D+=F.length,H)Q?.(D/H)}let G=new Uint8Array(D),U=0;for(let R of $)G.set(R,U),U+=R.length;return Q?.(1),new _(G.buffer)}constructor(J){let Q=new DataView(J),X=new TextDecoder;if(X.decode(new Uint8Array(J,0,4))!==T)throw Error("not a ghost-typer model.bin");let H=Q.getUint32(4,!0);if(H!==A)throw Error(`model.bin version ${H}, runtime expects ${A}`);let M=Q.getUint32(8,!0),$=JSON.parse(X.decode(new Uint8Array(J,12,M))),D=12+M+(8-(12+M)%8)%8,G=new Map;for(let Z of $.sections)G.set(Z.name,Z);let U=(Z)=>{let K=G.get(Z);return K?new Uint8Array(J,D+K.offset,K.bytes):null},R=(Z,K)=>{let B=G.get(Z);return new K(J,D+B.offset,B.bytes/K.BYTES_PER_ELEMENT)},F=$.id_bytes===2?Uint16Array:Uint32Array;this.vocab=X.decode(U("vocab")).split(`
|
| 2 |
`),this.text=this.vocab.map((Z)=>L(Z)),this.pieceIndex=new Map(this.vocab.map((Z,K)=>[Z,K]));let O=U("oov_keys").length?X.decode(U("oov_keys")).split(`
|
| 3 |
+
`):[],q=U("oov_vals");this.oov=new Map(O.map((Z,K)=>[Z,q[K]/255])),this.nPrev=$.n_prev,this.order=this.nPrev+1,this.eosId=$.eos_id,this.unkId=$.unk_id,this.policy=$.policy,this.stopAfter=new Set($.stop_after),this.bytes=J.byteLength,this.base=R("base",Float32Array),this.orders=$.orders.map((Z)=>{let K=R(`count${Z.k}`,Uint16Array),B=new Uint32Array(Z.contexts+1);for(let V=0;V<Z.contexts;V++)B[V+1]=B[V]+K[V];let N=new Float32Array(Z.pairs);if($.deltas==="u8"){let V=U(`dvals${Z.k}`);for(let Y=0;Y<Z.pairs;Y++)N[Y]=V[Y]*Z.scale}else if($.deltas==="f32")N.set(R(`dvals${Z.k}`,Float32Array));else{let V=R(`dvals${Z.k}`,Uint16Array);for(let Y=0;Y<Z.pairs;Y++)N[Y]=k(V[Y])}return{k:Z.k,n:Z.contexts,ctx:R(`ctx${Z.k}`,F),offsets:B,dids:R(`dids${Z.k}`,F),dvals:N,support:G.has(`support${Z.k}`)?R(`support${Z.k}`,Float32Array):null}}),this.contexts=this.orders.reduce((Z,K)=>Z+K.n,0)}window(J){let Q=J.length-1,X=[];for(let H=this.nPrev;H>=0;H--)X.push(J[Math.max(Q-H,0)]);return X}find(J,Q){let X=0,H=J.n-1;while(X<=H){let M=X+H>>1,$=0;for(let D=0;D<J.k&&$===0;D++)$=J.ctx[M*J.k+D]-Q[D];if($===0)return M;if($<0)X=M+1;else H=M-1}return-1}dist(J){let Q=this.window(J),X=Q.join(","),H=this.distCache.get(X);if(H)return H;let M=Float64Array.from(this.base),$=0;for(let F of this.orders){let O=this.find(F,Q.slice(-F.k));if(O<0)continue;for(let q=F.offsets[O];q<F.offsets[O+1];q++)M[F.dids[q]]+=F.dvals[q];$=F.support?F.support[O]:1/0}let D=-1/0;for(let F of M)if(F>D)D=F;let G=0,U=new Float64Array(M.length);for(let F=0;F<M.length;F++)G+=U[F]=Math.exp(M[F]-D);for(let F=0;F<U.length;F++)U[F]/=G;if(this.distCache.size>2000)this.distCache.clear();let R={probs:U,support:$};return this.distCache.set(X,R),R}candidates(J){let Q=this.candCache.get(J);if(!Q){let X=[];for(let H=this.unkId+1;H<this.vocab.length;H++)if(this.vocab[H].startsWith(J))X.push(H);if(Q=Int32Array.from(X),this.candCache.size>5000)this.candCache.clear();this.candCache.set(J,Q)}return Q}steps(J,Q=this.policy.max_words){let X=S(J);if(!X.length)return[];let H=X[X.length-1],M=this.candidates(H);if(!M.length)return[];let $=[this.eosId,...X.slice(0,-1).map((q)=>this.pieceIndex.get(q)??this.unkId)],{probs:D,support:G}=this.dist($),U=M[0],R=0;for(let q of M)if(R+=D[q],D[q]>D[U])U=q;let F=(1-(this.oov.get(H)??0))*(D[U]/R),O=[{text:this.text[U].slice(L(H).length),prob:F,support:G}];$.push(U);while(!this.stopAfter.has(this.vocab[$[$.length-1]])&&O.length<Q){({probs:D,support:G}=this.dist($));let q=0;for(let Z=1;Z<D.length;Z++)if(D[Z]>D[q])q=Z;if(q===this.eosId)break;F*=D[q],O.push({text:this.text[q],prob:F,support:G}),$.push(q)}return O}static apply(J,Q){let X="",H=0,M=Math.min(J.length,Q.max_words);for(let $=0;$<M;$++){let D=J[$];if(D.support<Q.min_support)break;if($&&Q.ext!=null){if(D.prob/J[$-1].prob<Q.ext)break}else if(D.prob<Q.tau)break;X+=D.text,H=D.prob}return{ghost:X,prob:H}}suggest(J,Q=this.policy){let X=this.steps(J,Q.max_words);return{..._.apply(X,Q),steps:X}}}export{_ as CompiledAutocomplete};
|
model-f32.bin
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8ac2dcc26c69f91ddd3da62de8d473b08db00641f14e7bc0927e869fb30891d4
|
| 3 |
+
size 16777264
|
model.bin
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b49b81f934b345a8a27348ac8660747b12f7dc457ec58586b0966a49a5644a80
|
| 3 |
+
size 10688120
|
src/autocomplete.ts
CHANGED
|
@@ -12,7 +12,9 @@
|
|
| 12 |
|
| 13 |
import { decodePieces, preTokenize } from "./tokenizer.ts";
|
| 14 |
|
| 15 |
-
|
|
|
|
|
|
|
| 16 |
export type Step = { text: string; prob: number; support: number };
|
| 17 |
export type Suggestion = { ghost: string; prob: number; steps: Step[] };
|
| 18 |
|
|
@@ -240,8 +242,13 @@ export class CompiledAutocomplete {
|
|
| 240 |
static apply(steps: Step[], policy: Policy): { ghost: string; prob: number } {
|
| 241 |
let ghost = "";
|
| 242 |
let prob = 0;
|
| 243 |
-
|
| 244 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
ghost += st.text;
|
| 246 |
prob = st.prob;
|
| 247 |
}
|
|
|
|
| 12 |
|
| 13 |
import { decodePieces, preTokenize } from "./tokenizer.ts";
|
| 14 |
|
| 15 |
+
// tau: min probability of the first word (and of the whole suggestion when ext is null/absent);
|
| 16 |
+
// ext: min probability of each word after the first.
|
| 17 |
+
export type Policy = { tau: number; min_support: number; max_words: number; ext?: number | null };
|
| 18 |
export type Step = { text: string; prob: number; support: number };
|
| 19 |
export type Suggestion = { ghost: string; prob: number; steps: Step[] };
|
| 20 |
|
|
|
|
| 242 |
static apply(steps: Step[], policy: Policy): { ghost: string; prob: number } {
|
| 243 |
let ghost = "";
|
| 244 |
let prob = 0;
|
| 245 |
+
const n = Math.min(steps.length, policy.max_words);
|
| 246 |
+
for (let i = 0; i < n; i++) {
|
| 247 |
+
const st = steps[i];
|
| 248 |
+
if (st.support < policy.min_support) break;
|
| 249 |
+
if (i && policy.ext != null) {
|
| 250 |
+
if (st.prob / steps[i - 1].prob < policy.ext) break;
|
| 251 |
+
} else if (st.prob < policy.tau) break;
|
| 252 |
ghost += st.text;
|
| 253 |
prob = st.prob;
|
| 254 |
}
|