From 729144096cc2321365b2df7382d37b04bd97d9a1 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Tue, 8 Sep 2026 17:20:33 -0400 Subject: [PATCH] =?UTF-8?q?laklok=20vector:=20the=20sister=20draws=20the?= =?UTF-8?q?=20same=20speaker=20=E2=80=94=20one=20glyph,=20two=20renderers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The vector chip carried the word while the raster carried the drawing. It now builds the same cone and waves as SVG rects on the raster's own pixel grid, at 2x and unsmoothed, tinted through currentColor so the four states stay identical. Parity gains the row table and the wave offsets (17 checks), so the glyph cannot drift on one side alone. Claude-Session: https://claude.ai/code/session_01GmNKmbpGxYo3NqtqyFosjY --- system/public/html/index.html | 44 +++++++++++++++++++++++++---- toolchain/laklok-sisters/parity.mjs | 20 +++++++++++++ 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/system/public/html/index.html b/system/public/html/index.html index ce977615ae..86bb1599a4 100644 --- a/system/public/html/index.html +++ b/system/public/html/index.html @@ -218,11 +218,14 @@ /* πŸ—£οΈ Pruttivox β€” @prutti's messages carry a `vox` chip that speaks them in his consented voice clone; the word being spoken lights up lime. Colors mirror the raster sister (disks/chat.mjs voxlink/voxword). */ - .voxchip { background: #ffaa3c; color: #3a2408; font: 600 10px var(--sans); padding: 1px 5px; - border-radius: 3px; margin-left: 6px; cursor: pointer; border: 0; vertical-align: middle; } - .voxchip:hover { background: #ffdc78; } - .voxchip.loading { background: #ffebb4; } - .voxchip.playing { background: #beff50; } + .voxchip { background: none; border: 0; padding: 0 0 0 1px; margin-left: 6px; + cursor: pointer; color: #ffaa3c; line-height: 0; vertical-align: middle; } + .voxchip:hover { color: #ffdc78; } + .voxchip.loading { color: #ffebb4; } + .voxchip.playing { color: #beff50; } + /* The glyph is drawn on the raster sister's pixel grid, so it must not be + smoothed on the way up to DOM size. */ + .voxchip svg { display: block; fill: currentColor; shape-rendering: crispEdges; } .body .lit, .body a.lit { color: #beff50 !important; } .body { word-break: break-word; } .time { color: var(--time); font-size: 11px; opacity: .8; margin-left: 2px; } @@ -801,6 +804,33 @@ function ago(when) { // URL counting as one word (the voice says "link"), matching the raster // sister and the server tokenizer. Highlight clocks on audio.currentTime. const VOX_HANDLE = "prutti"; +// The speaker glyph, mirrored pixel for pixel from the raster sister +// (VOX_CONE_ROWS in disks/chat.mjs): a cone in cols 0–3, then two waves at +// cols 5 and 7. Drawn at 2Γ— so it carries at DOM sizes; parity.mjs diffs the +// row table against chat.mjs, so edit both or neither. +const VOX_CONE_ROWS = [1, 2, 3, 4, 3, 2, 1]; // Row widths, all left-aligned. +const VOX_ICON_W = 8; +const VOX_ICON_H = 7; +const SVG_NS = "http://www.w3.org/2000/svg"; + +function voxGlyph() { + const svg = document.createElementNS(SVG_NS, "svg"); + svg.setAttribute("viewBox", `0 0 ${VOX_ICON_W} ${VOX_ICON_H}`); + svg.setAttribute("width", VOX_ICON_W * 2); + svg.setAttribute("height", VOX_ICON_H * 2); + svg.setAttribute("aria-hidden", "true"); + const rect = (x, y, w, h) => { + const r = document.createElementNS(SVG_NS, "rect"); + r.setAttribute("x", x); r.setAttribute("y", y); + r.setAttribute("width", w); r.setAttribute("height", h); + svg.append(r); + }; + VOX_CONE_ROWS.forEach((w, row) => rect(0, row, w, 1)); + rect(5, 2, 1, 3); // Near wave. + rect(7, 1, 1, 5); // Far wave. + return svg; +} + let vox = null; // { id, audio, chip, spans, words, raf } function voxStop() { @@ -928,8 +958,10 @@ function rowFor(msg) { if (msg.id && !msg.deleted && (msg.from || "").replace(/^@/, "").toLowerCase() === VOX_HANDLE) { const chip = document.createElement("button"); - chip.className = "voxchip"; chip.textContent = "vox"; + chip.className = "voxchip"; + chip.append(voxGlyph()); chip.title = "hΓΈr beskeden (syntetisk stemme)"; + chip.setAttribute("aria-label", "hΓΈr beskeden (syntetisk stemme)"); chip.onclick = () => voxToggle(row, chip); body.append(chip); } diff --git a/toolchain/laklok-sisters/parity.mjs b/toolchain/laklok-sisters/parity.mjs index 357db1079e..7edb518552 100644 --- a/toolchain/laklok-sisters/parity.mjs +++ b/toolchain/laklok-sisters/parity.mjs @@ -153,6 +153,26 @@ function regexLiteral(src, constName) { `chat.mjs: ${a ? "ok" : "CHANGED"} Β· vector: ${b ? "ok" : "CHANGED"}`); } +// ── 10. Vox speaker glyph β€” same pixels on both sisters ─────────────────── +{ + const rows = (src) => + src.match(/const VOX_CONE_ROWS = \[([^\]]*)\]/)?.[1]?.replace(/\s/g, ""); + const a = rows(chatDisk); + const b = rows(vector); + check("vox cone rows (VOX_CONE_ROWS in both)", !!a && a === b, + a === b ? a : `chat.mjs: ${a} Β· vector: ${b}`); + + // The two waves are drawn, not tabulated, so anchor their offsets too. One + // sister offsets from the draw origin (`x + 5`), the other from the SVG's + // own grid (`5`) β€” same pixels, so normalize the origin away before diffing. + const waves = (src) => { + const flat = src.replace(/[xy] \+ /g, ""); + return /\b5, 2, 1, 3\b/.test(flat) && /\b7, 1, 1, 5\b/.test(flat); + }; + check("vox wave offsets (cols 5 and 7)", waves(chatDisk) && waves(vector), + `chat.mjs: ${waves(chatDisk) ? "ok" : "CHANGED"} Β· vector: ${waves(vector) ? "ok" : "CHANGED"}`); +} + // ── Report ──────────────────────────────────────────────────────────────── let failed = 0; for (const { name, ok, detail } of checks) { -- 2.51.2