| /** | |
| * UI-side previews of what a watermark does to a single decoding step. | |
| * These are display helpers; the authoritative computation happens in the | |
| * worker's generation loop. | |
| */ | |
| /** | |
| * Probabilities after adding `delta` to green logits, renormalized. | |
| * | |
| * Exact renormalization needs the full vocabulary. We only ship the top-N | |
| * candidates to the UI, so the un-shown tail is approximated by its expected | |
| * behaviour: a fraction `gamma` of the tail mass is green and gets e^delta. | |
| * With the top-N holding most of the mass this is visually indistinguishable | |
| * from the exact curve, and the shape (green up, red down) is exact. | |
| */ | |
| export function biasedProbs( | |
| probs: number[], | |
| isGreen: boolean[], | |
| delta: number, | |
| gamma: number, | |
| ): number[] { | |
| const e = Math.exp(delta); | |
| let shown = 0; | |
| let weighted = 0; | |
| for (let i = 0; i < probs.length; i++) { | |
| shown += probs[i]; | |
| weighted += probs[i] * (isGreen[i] ? e : 1); | |
| } | |
| const tail = Math.max(0, 1 - shown); | |
| const z = weighted + tail * (1 - gamma + gamma * e); | |
| if (z <= 0) return probs.slice(); | |
| return probs.map((p, i) => (p * (isGreen[i] ? e : 1)) / z); | |
| } | |
| /** Gumbel-max score R^(1/p), returned as log for numeric stability. */ | |
| export function gumbelLogScore(r: number, p: number): number { | |
| if (p <= 0) return -Infinity; | |
| return Math.log(r) / p; | |
| } | |