| const PRESETS = [ |
| { |
| label: "A Litany for Survival ©️ Audre Lorde", |
| text: `For those of us who live at the shoreline |
| standing upon the constant edges of decision |
| crucial and alone |
| for those of us who cannot indulge |
| the passing dreams of choice |
| who love in doorways coming and going |
| in the hours between dawns |
| looking inward and outward |
| at once before and after |
| seeking a now that can breed |
| futures |
| like bread in our children’s mouths |
| so their dreams will not reflect |
| the death of ours; |
| |
| For those of us |
| who were imprinted with fear |
| like a faint line in the center of our foreheads |
| learning to be afraid with our mother’s milk |
| for by this weapon |
| this illusion of some safety to be found |
| the heavy-footed hoped to silence us |
| For all of us |
| this instant and this triumph |
| We were never meant to survive. |
| |
| And when the sun rises we are afraid |
| it might not remain |
| when the sun sets we are afraid |
| it might not rise in the morning |
| when our stomachs are full we are afraid |
| of indigestion |
| when our stomachs are empty we are afraid |
| we may never eat again |
| when we are loved we are afraid |
| love will vanish |
| when we are alone we are afraid |
| love will never return |
| and when we speak we are afraid |
| our words will not be heard |
| nor welcomed |
| but when we are silent |
| we are still afraid |
| |
| So it is better to speak |
| remembering |
| we were never meant to survive.`, |
| }, |
| { |
| label: "Wild Geese ©️ Mary Oliver", |
| text: `You do not have to be good. |
| You do not have to walk on your knees |
| for a hundred miles through the desert repenting. |
| You only have to let the soft animal of your body |
| love what it loves. |
| Tell me about despair, yours, and I will tell you mine. |
| Meanwhile the world goes on. |
| Meanwhile the sun and the clear pebbles of the rain |
| are moving across the landscapes, |
| over the prairies and the deep trees, |
| the mountains and the rivers. |
| Meanwhile the wild geese, high in the clean blue air, |
| are heading home again. |
| Whoever you are, no matter how lonely, |
| the world offers itself to your imagination, |
| calls to you like the wild geese, harsh and exciting - |
| over and over announcing your place |
| in the family of things.`, |
| }, |
| { |
| label: "i love you to the moon & ©️ Chen Chen", |
| text: `not back, let’s not come back, let’s go by the speed of |
| queer zest & stay up |
| there & get ourselves a little |
| moon cottage (so pretty), then start a moon garden |
| |
| with lots of moon veggies (so healthy), i mean |
| i was already moonlighting |
| as an online moonologist |
| most weekends, so this is the immensely |
| |
| logical next step, are you |
| packing your bags yet, don’t forget your |
| sailor moon jean jacket, let’s wear |
| our sailor moon jean jackets while twirling in that lighter, |
| |
| queerer moon gravity, let’s love each other |
| (so good) on the moon, let’s love |
| the moon |
| on the moon`, |
| }, |
| ]; |
|
|
| const poemEl = document.getElementById("poem"); |
| const loadBtn = document.getElementById("load"); |
| const condenseBtn = document.getElementById("condense"); |
| const expandBtn = document.getElementById("expand"); |
| const readtimeEl = document.getElementById("readtime"); |
| const statusEl = document.getElementById("status"); |
| const presetsEl = document.getElementById("presets"); |
|
|
| const presetBtns = PRESETS.map((preset) => { |
| const btn = document.createElement("button"); |
| btn.textContent = preset.label; |
| btn.addEventListener("click", () => { |
| poemEl.value = preset.text; |
| updateReadingTime(); |
| }); |
| presetsEl.append(btn); |
| return btn; |
| }); |
|
|
| poemEl.value = PRESETS[0].text; |
|
|
| let ready = false; |
| let busy = false; |
|
|
| const worker = new Worker("worker.js", { type: "module" }); |
| const pending = new Map(); |
| let nextId = 1; |
|
|
| worker.onmessage = (e) => { |
| const msg = e.data; |
| if (msg.type === "progress") { |
| onLoadProgress(msg.data); |
| } else if (msg.type === "ready") { |
| ready = true; |
| setWorking(loadBtn, null); |
| loadBtn.style.display = "none"; |
| console.log("tl;cr backend:", msg.device); |
| updateButtons(); |
| } else if (msg.id && pending.has(msg.id)) { |
| const { resolve, reject } = pending.get(msg.id); |
| pending.delete(msg.id); |
| if (msg.type === "error") reject(new Error(msg.message)); |
| else resolve(msg); |
| } else if (msg.type === "error") { |
| showError(msg.message); |
| setWorking(loadBtn, null); |
| loadBtn.disabled = false; |
| } |
| }; |
|
|
| worker.onerror = (e) => { |
| showError(`worker error: ${e.message}`); |
| setWorking(loadBtn, null); |
| loadBtn.disabled = false; |
| }; |
|
|
| function call(payload) { |
| return new Promise((resolve, reject) => { |
| const id = nextId++; |
| pending.set(id, { resolve, reject }); |
| worker.postMessage({ ...payload, id }); |
| }); |
| } |
|
|
| |
| function showError(text) { |
| statusEl.textContent = text ? `error: ${text}` : ""; |
| } |
|
|
| |
| function setWorking(btn, fraction, tooltip = "") { |
| if (fraction === null) { |
| btn.classList.remove("working"); |
| btn.style.removeProperty("--progress"); |
| btn.title = ""; |
| } else { |
| btn.classList.add("working"); |
| btn.style.setProperty("--progress", `${(fraction * 100).toFixed(1)}%`); |
| btn.title = tooltip; |
| } |
| } |
|
|
| function onLoadProgress({ status, file, loaded, total }) { |
| if (status === "progress" && file?.endsWith(".onnx") && total) { |
| const mb = (n) => (n / 1e6).toFixed(0); |
| setWorking(loadBtn, loaded / total, `downloading ${file}: ${mb(loaded)} / ${mb(total)} MB`); |
| } |
| } |
|
|
| function updateButtons() { |
| condenseBtn.disabled = busy || !ready; |
| expandBtn.disabled = busy || !ready; |
| |
| presetBtns.forEach((btn) => (btn.disabled = busy)); |
| } |
|
|
| function updateReadingTime() { |
| const words = poemEl.value.trim().split(/\s+/).filter(Boolean).length; |
| const seconds = Math.round((words / 200) * 60); |
| let span; |
| if (seconds < 60) { |
| span = `${seconds} second${seconds === 1 ? "" : "s"}`; |
| } else { |
| const m = Math.floor(seconds / 60); |
| const s = seconds % 60; |
| span = `${m} minute${m === 1 ? "" : "s"}${s ? ` ${s} second${s === 1 ? "" : "s"}` : ""}`; |
| } |
| readtimeEl.textContent = `Estimated reading time: ${span}`; |
| } |
|
|
| poemEl.addEventListener("input", updateReadingTime); |
| updateReadingTime(); |
|
|
| |
| |
| |
|
|
| function parsePoem(text) { |
| const stanzas = []; |
| let current = []; |
| for (const rawLine of text.split("\n")) { |
| const line = rawLine.trim(); |
| if (line === "") { |
| if (current.length) stanzas.push(current); |
| current = []; |
| } else { |
| current.push(line); |
| } |
| } |
| if (current.length) stanzas.push(current); |
| return stanzas; |
| } |
|
|
| |
| |
| function mergeLoneStanzas(stanzas) { |
| const merged = []; |
| let prevLone = false; |
| for (const stanza of stanzas) { |
| if (stanza.length === 1 && prevLone) merged.at(-1).push(stanza[0]); |
| else merged.push([...stanza]); |
| prevLone = stanza.length === 1; |
| } |
| return merged; |
| } |
|
|
| function renderPoem(stanzas) { |
| return stanzas |
| .filter((s) => s.length) |
| .map((s) => s.join("\n")) |
| .join("\n\n"); |
| } |
|
|
| |
| function flatten(stanzas) { |
| const flat = []; |
| stanzas.forEach((stanza, si) => |
| stanza.forEach((line, li) => flat.push({ si, li, line })), |
| ); |
| return flat; |
| } |
|
|
| function contextAround(flat, startGlobal, span) { |
| return { |
| before: flat.slice(Math.max(0, startGlobal - 2), startGlobal).map((f) => f.line), |
| after: flat.slice(startGlobal + span, startGlobal + span + 2).map((f) => f.line), |
| }; |
| } |
|
|
| |
|
|
| async function condensePass() { |
| const stanzas = mergeLoneStanzas(parsePoem(poemEl.value)); |
| const flat = flatten(stanzas); |
| const totalPairs = stanzas.reduce((n, s) => n + Math.floor(s.length / 2), 0); |
| if (totalPairs === 0) return; |
|
|
| const result = stanzas.map(() => []); |
| let globalIndex = 0; |
| let done = 0; |
| for (let si = 0; si < stanzas.length; si++) { |
| const stanza = stanzas[si]; |
| for (let i = 0; i < stanza.length; i += 2) { |
| if (i + 1 >= stanza.length) { |
| |
| result[si].push(stanza[i]); |
| globalIndex += 1; |
| continue; |
| } |
| setWorking(condenseBtn, done / totalPairs, `condensing pair ${done + 1} of ${totalPairs}`); |
| const { before, after } = contextAround(flat, globalIndex, 2); |
| const { line } = await call({ |
| type: "condense", |
| pair: [stanza[i], stanza[i + 1]], |
| before, |
| after, |
| }); |
| result[si].push(line); |
| globalIndex += 2; |
| done += 1; |
| renderLive(stanzas, result, si, i + 2); |
| } |
| } |
|
|
| poemEl.value = renderPoem(mergeLoneStanzas(result)); |
| updateReadingTime(); |
| } |
|
|
| async function expandPass() { |
| const stanzas = parsePoem(poemEl.value); |
| const flat = flatten(stanzas); |
| const totalLines = flat.length; |
| if (totalLines === 0) return; |
|
|
| const result = stanzas.map(() => []); |
| let globalIndex = 0; |
| for (let si = 0; si < stanzas.length; si++) { |
| const stanza = stanzas[si]; |
| for (let i = 0; i < stanza.length; i++) { |
| setWorking(expandBtn, globalIndex / totalLines, `expanding line ${globalIndex + 1} of ${totalLines}`); |
| const { before, after } = contextAround(flat, globalIndex, 1); |
| const { lines } = await call({ |
| type: "expand", |
| line: stanza[i], |
| before, |
| after, |
| }); |
| result[si].push(...lines); |
| globalIndex += 1; |
| renderLive(stanzas, result, si, i + 1); |
| } |
| } |
|
|
| poemEl.value = renderPoem(result); |
| updateReadingTime(); |
| } |
|
|
| |
| |
| function renderLive(stanzas, result, si, consumedInStanza) { |
| const view = []; |
| for (let k = 0; k < si; k++) view.push(result[k]); |
| view.push([...result[si], ...stanzas[si].slice(consumedInStanza)]); |
| for (let k = si + 1; k < stanzas.length; k++) view.push(stanzas[k]); |
| poemEl.value = renderPoem(view); |
| updateReadingTime(); |
| } |
|
|
| |
|
|
| loadBtn.addEventListener("click", () => { |
| loadBtn.disabled = true; |
| setWorking(loadBtn, 0, "loading model…"); |
| showError(""); |
| worker.postMessage({ type: "load" }); |
| }); |
|
|
| async function runPass(btn, pass) { |
| if (busy || !ready) return; |
| busy = true; |
| updateButtons(); |
| showError(""); |
| try { |
| await pass(); |
| } catch (err) { |
| showError(err.message); |
| } finally { |
| setWorking(btn, null); |
| busy = false; |
| updateButtons(); |
| } |
| } |
|
|
| condenseBtn.addEventListener("click", () => runPass(condenseBtn, condensePass)); |
| expandBtn.addEventListener("click", () => runPass(expandBtn, expandPass)); |
|
|