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) {