openvibers/openvibe.live Commits

/tts messages show in chat and on the OBS overlay, tagged TTS (they were only spoken, so viewers heard text nobody had written) (#286)

OP OpenVibers committed 24 days ago · 5ae70f83d1 · 4 files, +50 −8

Changes

ddiff --git a/public/css/chat-shared.css b/public/css/chat-shared.css
iindex 15788845..bbb07abd 100644
---- a/public/css/chat-shared.css
++++ b/public/css/chat-shared.css
@@@ -490,3 +490,13 @@
cursor: auto;
user-select: text;
}
++
++/* A /tts message in chat: what was read aloud, tagged. */
++.chat-tts-tag {
++ display: inline-flex; align-items: center; gap: 0.3em;
++ padding: 0 0.45em; border-radius: 1em;
++ background: color-mix(in srgb, var(--accent, #3b82f6) 18%, transparent);
++ color: var(--accent-light, #93c5fd);
++ font-size: 0.8em; font-weight: 700; letter-spacing: 0.03em;
++ vertical-align: 0.05em;
++}
ddiff --git a/public/js/chat.js b/public/js/chat.js
iindex 1c50e823..105d982c 100644
---- a/public/js/chat.js
++++ b/public/js/chat.js
@@@ -784,14 +784,9 @@ function isBroadcastingLiveForTTS() {
}
// ── Own-channel TTS: one speaking tab per device ─────────────────────────────
--// "TTS On My Channel When Live" must play in exactly ONE tab per browser no matter
--// how many chats/popouts the streamer has open — otherwise every tab speaks every
--// message on top of each other. A tab showing the streamer's OWN channel chat (or
--// their live broadcast page) takes a Web Lock and becomes the speaker; the rest stand
--// by and take over automatically when it closes or moves to another channel. Only
--// those tabs compete: every tab used to ask at load, so a home-page or other-channel
--// tab opened first held the lock for good and the streamer's own popout never spoke
--// (owner, 2026-10-18). Browsers without the Web Locks API keep every-tab behavior.
++// One tab per browser speaks the streamer's own-channel TTS: a tab showing their own chat (or broadcast page)
++// takes a Web Lock; the rest stand by and take over when it closes or leaves (owner, 2026-10-18). No Web Locks:
++// every tab speaks.
const _ownTtsLocks = typeof navigator !== 'undefined' && 'locks' in navigator;
let _ownTtsLeader = !_ownTtsLocks;
let _ownTtsRelease = null; // ends this tab's held (or queued) lock request
@@@ -3372,6 +3367,8 @@ function handleChatMessage(msg) {
break;
}
case 'tts':
++ // Shown as a chat line too (people heard /tts and saw nothing); speaking below is unchanged.
++ addChatMessage({ ...msg, type: 'chat', message_type: 'tts' });
// Legacy browser-side TTS (Self TTS mode)
// Only use broadcast TTS path when viewing own channel chat
if (_isViewingOwnBroadcastChat() && broadcastState?.settings?.ttsMode === 'self') {
@@@ -3734,6 +3731,7 @@ function buildChatMessageEl(msg, opts = {}) {
</div>
`;
}
++ if (msg.message_type === 'tts') text = `<span class="chat-tts-tag" title="Read aloud (/tts)"><i class="fa-solid fa-volume-high"></i> TTS</span> ${text}`;
if (msg.message_type === 'channel-sound') {
const cmd = esc((msg.sound && msg.sound.command) || '');
const sSpeed = Number((msg.sound && msg.sound.speed) || 1);
ddiff --git a/public/obs/chat.html b/public/obs/chat.html
iindex cc236123..245aecfb 100644
---- a/public/obs/chat.html
++++ b/public/obs/chat.html
@@@ -591,6 +591,8 @@ body.no-shadow .msg { text-shadow: none; }
return;
}
++ // A /tts message shows as a chat line too, tagged, so viewers see what is being read (owner, 2026-10-20).
++ if (msg.type === 'tts') msg = { ...msg, type: 'chat', message_type: 'tts' };
if (msg.type !== 'chat') return;
const rawName = msg.username || msg.displayName || (msg.anonId ? `anon${msg.anonId}` : 'anon');
@@@ -642,6 +644,7 @@ body.no-shadow .msg { text-shadow: none; }
text = `<span class="channel-sound-pill"><i class="fa-solid fa-volume-high"></i> played !${cmd}</span>`;
} else {
text = parseEmotes(rawText);
++ if (msg.message_type === 'tts') text = `<span class="channel-sound-pill tts-pill"><i class="fa-solid fa-volume-high"></i> TTS</span> ${text}`;
}
const sep = msg.message_type === 'channel-sound' ? ' ' : ': ';
ddiff --git a/test/tts-in-chat.test.js b/test/tts-in-chat.test.js
nnew file mode 100644
iindex 00000000..29f76fbe
---- /dev/null
++++ b/test/tts-in-chat.test.js
@@@ -0,0 +1,31 @@
++/**
++ * /tts messages show in chat (owner, 2026-10-20: "the /tts command doesnt show in chat so people are like wtf").
++ * OpenVibe.Chat sends a /tts as a `tts` event only (speech); nothing put it in the chat. Live's chat and the OBS chat
++ * overlay now show it as a chat line tagged "TTS", and speaking it is unchanged (the line is added directly, not
++ * through the `chat` case, so a broadcaster's self-TTS mode does not read it a second time).
++ *
++ * node test/tts-in-chat.test.js
++ */
++'use strict';
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++
++const root = path.join(__dirname, '..');
++const read = (p) => fs.readFileSync(path.join(root, p), 'utf8');
++
++const chat = read('public/js/chat.js');
++// The main socket's tts case adds the line before it decides whether this tab speaks.
++const ttsCase = /case 'tts':\n\s*\/\/ Shown as a chat line too[^\n]*\n\s*addChatMessage\(\{ \.\.\.msg, type: 'chat', message_type: 'tts' \}\);\n\s*\/\/ Legacy browser-side TTS/.exec(chat);
++assert.ok(ttsCase, "the main socket's tts case shows the message in chat first");
++assert.strictEqual(chat.split("addChatMessage({ ...msg, type: 'chat', message_type: 'tts' })").length - 1, 1, 'once (the broadcast page\'s background socket does not add a second line)');
++assert.ok(chat.includes(`if (msg.message_type === 'tts') text = \`<span class="chat-tts-tag" title="Read aloud (/tts)"><i class="fa-solid fa-volume-high"></i> TTS</span> \${text}\`;`), 'tagged TTS');
++
++// The OBS overlay shows it the same way.
++const obs = read('public/obs/chat.html');
++assert.ok(obs.includes("if (msg.type === 'tts') msg = { ...msg, type: 'chat', message_type: 'tts' };"));
++assert.ok(obs.indexOf("if (msg.type === 'tts') msg =") < obs.indexOf("if (msg.type !== 'chat') return;"), 'converted before non-chat events are dropped');
++assert.match(obs, /if \(msg\.message_type === 'tts'\) text = `<span class="channel-sound-pill tts-pill">/);
++
++assert.match(read('public/css/chat-shared.css'), /\.chat-tts-tag \{/);
++console.log('tts in chat: /tts messages show in chat and on the overlay, tagged — all checks passed');