openvibers/openvibe.live Commits

Browser TTS reads an emote by its name: no brackets, colons or numeric ids (#626)

OP OpenVibers committed 7 days ago · 841bd969fc · 3 files, +43 −3

* Browser TTS reads an emote by its name: no brackets, colons or numeric ids

The owner, 2026-11-06: Kick emotes were spoken with their colons and number ids. chat-shared.js ttsReplaceLinks (used
by chat.js speakTTS and broadcast.js's queue) now runs ttsSpeakableEmotes first, the same rule as OpenVibe.Chat #359
on the server: Kick [emote:id:Name] / [emoji:Name], Discord <:name:id> / <a:name:id> and :shortcode: become the name,
underscores as spaces, and three or more of the same word in a row are read once.

* Perf budget: jsBrotliKB 302.2 (302.1 measured with TTS speaking emotes by name)

Changes

ddiff --git a/public/js/chat-shared.js b/public/js/chat-shared.js
iindex 3b224ee8..742e3a79 100644
---- a/public/js/chat-shared.js
++++ b/public/js/chat-shared.js
@@@ -522,10 +522,30 @@
if (n.length > 18) n = n.slice(0, 18);
return n;
}
-- // Replace only REAL links with a spoken description; leave non-link text (typos) untouched.
++ // Emotes are spoken by their name only (the owner, 2026-11-06; the same rule as OpenVibe.Chat's tts-engine
++ // speakableEmotes): Kick's [emote:123456:Name] / [emoji:Name], Discord's <:name:123> / <a:name:123> and :shortcode:
++ // become the name, underscores read as spaces; a word said three or more times in a row is read once.
++ function ttsSpeakableEmotes(text) {
++ const name = (n) => ' ' + String(n).replace(/_+/g, ' ').trim() + ' ';
++ const t = String(text == null ? '' : text)
++ .replace(/\[(?:emote|emoji):(?:\d+:)?([A-Za-z0-9_]{1,64})\]/g, (_m, n) => name(n))
++ .replace(/<a?:([A-Za-z0-9_]{1,64}):\d+>/g, (_m, n) => name(n))
++ .replace(/(^|\s):([A-Za-z0-9_+-]{2,32}):(?=\s|$)/g, (_m, pre, n) => pre + name(n));
++ const words = t.split(/\s+/).filter(Boolean);
++ const out = [];
++ for (let i = 0; i < words.length;) {
++ let j = i;
++ while (j + 1 < words.length && words[j + 1].toLowerCase() === words[i].toLowerCase()) j++;
++ out.push(words[i]);
++ if (j - i + 1 === 2) out.push(words[i + 1]);
++ i = j + 1;
++ }
++ return out.join(' ');
++ }
++ // Replace only REAL links with a spoken description; leave non-link text (typos) untouched. Emotes first.
function ttsReplaceLinks(text, username) {
const uname = ttsUsername(username);
-- return String(text == null ? '' : text).replace(
++ return ttsSpeakableEmotes(text).replace(
/((?:https?:\/\/)?(?:www\.)?)([a-z0-9][-a-z0-9]*(?:\.[a-z]{2,})+)([^\s]*)/gi,
(m, pre, domain) => {
const hasScheme = /https?:\/\//i.test(pre) || /^www\./i.test(pre);
@@@ -541,6 +561,7 @@
escape: esc,
linkify,
ttsReplaceLinks,
++ ttsSpeakableEmotes,
ttsUsername,
handleLinkClick: handleChatLinkClick,
showLinkContextMenu,
ddiff --git a/scripts/perf/check-budgets.js b/scripts/perf/check-budgets.js
iindex 16d30ea1..b1aa3f7c 100644
---- a/scripts/perf/check-budgets.js
++++ b/scripts/perf/check-budgets.js
@@@ -23,7 +23,7 @@ const BUDGETS = {
htmlBrotliKB: 30,
jsFiles: 26, // measured 23 (incl. route scripts for "/")
jsRawKB: 1290, // 1280.1 (2026-11-03: the chat name card shows at once, the logs give up after ten seconds; headroom for openvibe-shared 4.107.3); 1279.2 (2026-11-03: openvibe-shared 4.107.0, open.<tld> brands; the home's reveal scroll listener hooked once); 1278.3 (2026-11-02: openvibe-shared 4.106.2, the balances stack so the site's name reads whole on a phone); 1277.1 (2026-11-02: the home's money four as one 'None yet' chip while all four are zero, open item 135); 1275.2 (2026-11-02: route placeholders in ov-skeleton.js, L52 step 6: a loading route shows its shape, not an empty box); 1268.4 (2026-11-02: openvibe-shared 4.103.0, the puzzle glyph, two-line sidebar rows, boost's fix and eight solids; the 404 offers the channel for a bare /name); 1266.3 (2026-11-02: openvibe-shared 4.95.0, keyboard-reachable scrollers; the phone Pastes rail); 1262.1 (2026-11-02: openvibe-shared 4.94.0, the Audio chip, the go-live card's labels); 1259.8 (2026-11-02: content labels on cards and the player, L1); 1257.5 (2026-11-01: the Catch up rails load by themselves as you swipe, no Load more on a phone); 1254.2 (2026-11-01: openvibe-shared 4.87.0), about 1255 with #534 (feed and card avatars at their size); 1253.8 (2026-11-01: avatars ask Media for their drawn width, ovImgW); 1252.3 (2026-11-01: Following in the topics row, the picked topic remembered); 1250.2 (2026-11-01: topics live now, readable chat names on light themes); 1247.7 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 1244.3 with openvibe-shared 4.66.0: the mark with depth and motion, ov-fx loaded on request (2026-10-31); 1243.5 with openvibe-shared 4.64.0: the look travels between sites, the Look panel in the menu (2026-10-31); 1242.7 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face, and the ring on page moves (2026-10-31); 1238.8 with the home's featured stream once and the intro closed on phones (2026-10-31); 1237.4 with the home's two more tab blocks on phones (2026-10-30); 1233.9 with the home's Catch up tabs on phones (2026-10-30); 1230.8 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 1227.8 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 1223.5 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 1215.4 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); measured 1203.9 (2026-10-28: openvibe-shared 4.30.0, the bell center: tabs with counts, the Unread filter, thumbnails, swipe, the phone sheet; 4.31.0's ✦ adds about 3 KB); 1187.2 (2026-10-28: openvibe-shared 4.27.0, Notifications 2.0: opaque themed toasts held while a menu is open, grouping, priority, System cards and the rich bell panel); 1158.8 (2026-10-28: openvibe-shared 4.25.0, the menu's orb, messages, the phone sheet and auto-hide in the navbar; the messenger opens from the bar)
-- jsBrotliKB: 302, // 301.7 with the agent stream's route (2026-11-06: the route in app.js and its shape in ov-skeleton.js; the agent's own scripts stay route-loaded, Live #622); 301.6 (2026-11-06: the Share button sweep — the shared kit's Share button markup in app.js, Live #617); 301.2 with openvibe-shared 4.114.0 (2026-11-06: the one Share sheet in the navbar (ov-share.js and ov-qr.js load on the first press); navbar.js gains its click handler); 300.1 (2026-11-03: every api() read gives up after 20 s, Live #589; openvibe-shared 4.107.3); 299.3 (2026-11-02: openvibe-shared 4.106.2, the balances stack); 298.4 (2026-11-02: route placeholders, L52 step 6); 296.7 (2026-11-02: openvibe-shared 4.103.0 and the 404's channel offer); 296.2 (2026-11-02: openvibe-shared 4.95.0, keyboard-reachable scrollers; the phone Pastes rail); 295.1 (2026-11-02: openvibe-shared 4.94.0, the Audio chip, the go-live card's labels); 294.6 (2026-11-02: content labels on cards and the player, L1); 294.1 (2026-11-01: the Catch up rails load by themselves as you swipe); 292.6 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 291.1 with openvibe-shared 4.57.1 (the Dock's async token and scope-aware controls, 4.57.0; the tool frame's facts, 4.56.0) (2026-10-31); 290.6 with the home's two more tab blocks on phones (2026-10-30); 289.8 with the home's Catch up tabs on phones (2026-10-30); 288.2 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 287.4 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); measured 283.2 (2026-10-28: openvibe-shared 4.30.0, the bell center as above); 279.6 (2026-10-28: openvibe-shared 4.27.0, Notifications 2.0 as above); 273.1 (2026-10-28: openvibe-shared 4.25.0 as above); 270.5 (2026-10-28: openvibe-shared 4.22.0, the navbar's search magnifier with / and Ctrl+K and the More menu; search.js and loading.js load only when used); 269.6 (2026-10-28: openvibe-shared 4.19.0, the account menu's gear and people-group icons inline; 268 on 2026-10-27: openvibe-shared 4.16.0, the folded balance chip, Across OpenVibe with site marks and the icon preload in the navbar); 265.3 (2026-10-27: openvibe-shared 4.15.0, the compact footer opens into the whole one, and the pulse line); 263.3 on 10-23 (openvibe-shared 4.9.0, status text twins in every theme); 262.5 on 10-17 (openvibe-shared 4.4.0, every site in the launcher)
++ jsBrotliKB: 302.2, // 302.1 (2026-11-06: TTS speaks emotes by name, ttsSpeakableEmotes in chat-shared.js, Live #626); 301.7 with the agent stream's route (2026-11-06: the route in app.js and its shape in ov-skeleton.js; the agent's own scripts stay route-loaded, Live #622); 301.6 (2026-11-06: the Share button sweep — the shared kit's Share button markup in app.js, Live #617); 301.2 with openvibe-shared 4.114.0 (2026-11-06: the one Share sheet in the navbar (ov-share.js and ov-qr.js load on the first press); navbar.js gains its click handler); 300.1 (2026-11-03: every api() read gives up after 20 s, Live #589; openvibe-shared 4.107.3); 299.3 (2026-11-02: openvibe-shared 4.106.2, the balances stack); 298.4 (2026-11-02: route placeholders, L52 step 6); 296.7 (2026-11-02: openvibe-shared 4.103.0 and the 404's channel offer); 296.2 (2026-11-02: openvibe-shared 4.95.0, keyboard-reachable scrollers; the phone Pastes rail); 295.1 (2026-11-02: openvibe-shared 4.94.0, the Audio chip, the go-live card's labels); 294.6 (2026-11-02: content labels on cards and the player, L1); 294.1 (2026-11-01: the Catch up rails load by themselves as you swipe); 292.6 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 291.1 with openvibe-shared 4.57.1 (the Dock's async token and scope-aware controls, 4.57.0; the tool frame's facts, 4.56.0) (2026-10-31); 290.6 with the home's two more tab blocks on phones (2026-10-30); 289.8 with the home's Catch up tabs on phones (2026-10-30); 288.2 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 287.4 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); measured 283.2 (2026-10-28: openvibe-shared 4.30.0, the bell center as above); 279.6 (2026-10-28: openvibe-shared 4.27.0, Notifications 2.0 as above); 273.1 (2026-10-28: openvibe-shared 4.25.0 as above); 270.5 (2026-10-28: openvibe-shared 4.22.0, the navbar's search magnifier with / and Ctrl+K and the More menu; search.js and loading.js load only when used); 269.6 (2026-10-28: openvibe-shared 4.19.0, the account menu's gear and people-group icons inline; 268 on 2026-10-27: openvibe-shared 4.16.0, the folded balance chip, Across OpenVibe with site marks and the icon preload in the navbar); 265.3 (2026-10-27: openvibe-shared 4.15.0, the compact footer opens into the whole one, and the pulse line); 263.3 on 10-23 (openvibe-shared 4.9.0, status text twins in every theme); 262.5 on 10-17 (openvibe-shared 4.4.0, every site in the launcher)
cssRawKB: 270, // measured ~243 since the first-paint subset (style.core.css) replaced style.css (~717 before)
cssBrotliKB: 55, // measured ~47.5 (was ~130)
};
ddiff --git a/test/tts-emotes-client.test.js b/test/tts-emotes-client.test.js
nnew file mode 100644
iindex 00000000..5a9a09cd
---- /dev/null
++++ b/test/tts-emotes-client.test.js
@@@ -0,0 +1,19 @@
++// Browser TTS (chat.js speakTTS, broadcast.js's queue) speaks an emote by its name only (the owner, 2026-11-06):
++// public/js/chat-shared.js ttsReplaceLinks runs ttsSpeakableEmotes first, the same rule as OpenVibe.Chat's server TTS.
++const assert = require('assert');
++global.window = {};
++global.document = { addEventListener() {}, querySelector() { return null; } };
++require('../public/js/chat-shared.js');
++const C = global.window.OpenVibeChat;
++const cases = [
++ ['lol [emote:37226:KEKLEO] that was funny', 'lol KEKLEO that was funny'],
++ ['[emoji:emojiSmile]', 'emojiSmile'],
++ ['hi <:pepe_hands:123456789> and <a:catJAM:42>', 'hi pepe hands and catJAM'],
++ ['nice :PogChamp: play', 'nice PogChamp play'],
++ ['[emote:1:KEKW] [emote:1:KEKW] [emote:1:KEKW] [emote:1:KEKW]', 'KEKW'],
++ ['no no', 'no no'],
++ ['time 10:30:45 ok', 'time 10:30:45 ok'],
++];
++cases.forEach(([input, want]) => assert.strictEqual(C.ttsSpeakableEmotes(input), want, input));
++assert.strictEqual(C.ttsReplaceLinks('gg [emote:37226:KEKLEO] see https://openvibe.live/x', 'bob'), 'gg KEKLEO see (bob sent a link to openvibe)', 'emotes first, then links');
++console.log(`tts-emotes-client: ${cases.length + 1} checks passed`);