openvibers/openvibe.live Commits
TTS: dedupe identical utterances at synth AND playback
Changes
@@@ -5001,7 +5001,7 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig<script src="/js/chat-shared.js?v=3" defer></script><script src="/js/emotes.js?v=8" defer></script><script src="/js/gif-picker.js?v=1" defer></script>--<script src="/js/chat.js?v=75" defer></script>++<script src="/js/chat.js?v=76" defer></script><script src="/js/chat-autocomplete.js?v=1" defer></script><script src="/js/channel-uploads.js?v=12" defer></script><script src="/js/controls.js?v=8" defer></script>@@@ -2640,6 +2640,7 @@ function _handleBgBroadcastMessage(msg) {break;case 'tts-audio':if (!isOwnChannelTtsSpeaker() || !isChatTTSEnabled({ streaming: true }) || !_isTTSEnabledForSource(msg.source_platform)) return;++ if (_ttsRecentlyPlayed(msg)) return; // bg socket + main socket can both deliverif (typeof playBroadcastTTSAudio === 'function' && typeof broadcastState !== 'undefined') {playBroadcastTTSAudio(msg);}@@@ -2861,7 +2862,8 @@ function handleChatMessage(msg) {// Server-synthesized TTS audio (Site-Wide TTS mode)// Only route through broadcast audio when on own channelif (_isViewingOwnBroadcastChat() && typeof playBroadcastTTSAudio === 'function') {-- if (isOwnChannelTtsSpeaker() && isChatTTSEnabled({ streaming: true }) && _isTTSEnabledForSource(msg.source_platform)) playBroadcastTTSAudio(msg);++ if (isOwnChannelTtsSpeaker() && isChatTTSEnabled({ streaming: true }) && _isTTSEnabledForSource(msg.source_platform)++ && !_ttsRecentlyPlayed(msg)) playBroadcastTTSAudio(msg);} else if (_isOwnChannelChat()) {if (isOwnChannelTtsSpeaker() && _ownChannelTtsWanted() && _isTTSEnabledForSource(msg.source_platform)) playTTSAudio(msg);} else if (isChatTTSEnabled() && !_isOwnTtsMessage(msg) && _isTTSEnabledForSource(msg.source_platform)) {@@@ -5822,10 +5824,27 @@ function _createJunglePitchShifter(context) {};}++// Last line of defense against double TTS: whatever the cause (two sockets in one++// tab, a duplicated relay bridge, a re-broadcast), the same speaker saying the same++// thing twice within a few seconds plays ONCE.++const _recentTtsPlays = new Map();++function _ttsRecentlyPlayed(msg) {++ const key = `${msg.username || ''}|${String(msg.message || msg.text || '').slice(0, 200)}`;++ const now = Date.now();++ const last = _recentTtsPlays.get(key);++ if (last && now - last < 8000) return true;++ _recentTtsPlays.set(key, now);++ if (_recentTtsPlays.size > 200) {++ for (const [k, t] of _recentTtsPlays) if (now - t > 30000) _recentTtsPlays.delete(k);++ }++ return false;++}++function playTTSAudio(msg) {if (!msg.audio || !msg.mimeType) return;// "." prefix = TTS explicitly skipped.if (String(msg.message || msg.text || '').trimStart().startsWith('.')) return;++ if (msg.type === 'tts-audio' && _ttsRecentlyPlayed(msg)) return;_ttsAudioQueue.push(msg);_processTTSAudioQueue();}@@@ -233,7 +233,7 @@<script src="/js/vibes.js?v=10"></script><script src="/js/opencoins.js?v=6"></script><script src="/js/cosmetics-ui.js?v=2"></script>-- <script src="/js/chat.js?v=75"></script>++ <script src="/js/chat.js?v=76"></script><script src="/js/chat-autocomplete.js?v=1"></script><script>'use strict';@@@ -1638,6 +1638,19 @@ class ChatServer {async synthesizeAndBroadcastTTS(streamId, username, text, voiceFX, sourcePlatform = null, identityKey = null, channelUserId = null) {// Queue accounting key: per-stream when live, per-channel when offline.const queueKey = streamId || (channelUserId ? `ch:${channelUserId}` : null);++ // Same utterance, same room, twice within a few seconds = a duplicated bridge++ // or double-fired relay (RS restart races have produced exactly this) — one++ // synthesis, one broadcast. Keyed per room so multi-slot mirrors still get++ // their own copy for their own viewers.++ if (!this._recentTtsDedupe) this._recentTtsDedupe = new Map();++ const dedupeKey = `${queueKey}|${identityKey || username}|${String(text).slice(0, 200)}`;++ const nowMs = Date.now();++ const lastMs = this._recentTtsDedupe.get(dedupeKey);++ if (lastMs && nowMs - lastMs < 8000) { console.log(`[TTS] deduped duplicate synth for ${username} in ${queueKey}`); return; }++ this._recentTtsDedupe.set(dedupeKey, nowMs);++ if (this._recentTtsDedupe.size > 500) {++ for (const [k, t] of this._recentTtsDedupe) if (nowMs - t > 30000) this._recentTtsDedupe.delete(k);++ }try {// "." prefix = user opted this message out of TTS — never synthesize or broadcast it.if (String(text || '').trimStart().startsWith('.')) return;
@@ -5001,7 +5001,7 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
@@ -5001,7 +5001,7 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
<script src="/js/chat-shared.js?v=3" defer></script>
<script src="/js/chat-shared.js?v=3" defer></script>
<script src="/js/emotes.js?v=8" defer></script>
<script src="/js/emotes.js?v=8" defer></script>
<script src="/js/gif-picker.js?v=1" defer></script>
<script src="/js/gif-picker.js?v=1" defer></script>
-<script src="/js/chat.js?v=75" defer></script>
+<script src="/js/chat.js?v=76" defer></script>
<script src="/js/chat-autocomplete.js?v=1" defer></script>
<script src="/js/chat-autocomplete.js?v=1" defer></script>
<script src="/js/channel-uploads.js?v=12" defer></script>
<script src="/js/channel-uploads.js?v=12" defer></script>
<script src="/js/controls.js?v=8" defer></script>
<script src="/js/controls.js?v=8" defer></script>
@@ -2640,6 +2640,7 @@ function _handleBgBroadcastMessage(msg) {
@@ -2640,6 +2640,7 @@ function _handleBgBroadcastMessage(msg) {
break;
break;
case 'tts-audio':
case 'tts-audio':
if (!isOwnChannelTtsSpeaker() || !isChatTTSEnabled({ streaming: true }) || !_isTTSEnabledForSource(msg.source_platform)) return;
if (!isOwnChannelTtsSpeaker() || !isChatTTSEnabled({ streaming: true }) || !_isTTSEnabledForSource(msg.source_platform)) return;
+ if (_ttsRecentlyPlayed(msg)) return; // bg socket + main socket can both deliver
if (typeof playBroadcastTTSAudio === 'function' && typeof broadcastState !== 'undefined') {
if (typeof playBroadcastTTSAudio === 'function' && typeof broadcastState !== 'undefined') {
playBroadcastTTSAudio(msg);
playBroadcastTTSAudio(msg);
}
}
@@ -2861,7 +2862,8 @@ function handleChatMessage(msg) {
@@ -2861,7 +2862,8 @@ function handleChatMessage(msg) {
// Server-synthesized TTS audio (Site-Wide TTS mode)
// Server-synthesized TTS audio (Site-Wide TTS mode)
// Only route through broadcast audio when on own channel
// Only route through broadcast audio when on own channel
if (_isViewingOwnBroadcastChat() && typeof playBroadcastTTSAudio === 'function') {
if (_isViewingOwnBroadcastChat() && typeof playBroadcastTTSAudio === 'function') {
- if (isOwnChannelTtsSpeaker() && isChatTTSEnabled({ streaming: true }) && _isTTSEnabledForSource(msg.source_platform)) playBroadcastTTSAudio(msg);
+ if (isOwnChannelTtsSpeaker() && isChatTTSEnabled({ streaming: true }) && _isTTSEnabledForSource(msg.source_platform)
+ && !_ttsRecentlyPlayed(msg)) playBroadcastTTSAudio(msg);
} else if (_isOwnChannelChat()) {
} else if (_isOwnChannelChat()) {
if (isOwnChannelTtsSpeaker() && _ownChannelTtsWanted() && _isTTSEnabledForSource(msg.source_platform)) playTTSAudio(msg);
if (isOwnChannelTtsSpeaker() && _ownChannelTtsWanted() && _isTTSEnabledForSource(msg.source_platform)) playTTSAudio(msg);
} else if (isChatTTSEnabled() && !_isOwnTtsMessage(msg) && _isTTSEnabledForSource(msg.source_platform)) {
} else if (isChatTTSEnabled() && !_isOwnTtsMessage(msg) && _isTTSEnabledForSource(msg.source_platform)) {
@@ -5822,10 +5824,27 @@ function _createJunglePitchShifter(context) {
@@ -5822,10 +5824,27 @@ function _createJunglePitchShifter(context) {
};
};
}
}
+// Last line of defense against double TTS: whatever the cause (two sockets in one
+// tab, a duplicated relay bridge, a re-broadcast), the same speaker saying the same
+// thing twice within a few seconds plays ONCE.
+const _recentTtsPlays = new Map();
+function _ttsRecentlyPlayed(msg) {
+ const key = `${msg.username || ''}|${String(msg.message || msg.text || '').slice(0, 200)}`;
+ const now = Date.now();
+ const last = _recentTtsPlays.get(key);
+ if (last && now - last < 8000) return true;
+ _recentTtsPlays.set(key, now);
+ if (_recentTtsPlays.size > 200) {
+ for (const [k, t] of _recentTtsPlays) if (now - t > 30000) _recentTtsPlays.delete(k);
+ }
+ return false;
+}
+
function playTTSAudio(msg) {
function playTTSAudio(msg) {
if (!msg.audio || !msg.mimeType) return;
if (!msg.audio || !msg.mimeType) return;
// "." prefix = TTS explicitly skipped.
// "." prefix = TTS explicitly skipped.
if (String(msg.message || msg.text || '').trimStart().startsWith('.')) return;
if (String(msg.message || msg.text || '').trimStart().startsWith('.')) return;
+ if (msg.type === 'tts-audio' && _ttsRecentlyPlayed(msg)) return;
_ttsAudioQueue.push(msg);
_ttsAudioQueue.push(msg);
_processTTSAudioQueue();
_processTTSAudioQueue();
}
}
@@ -233,7 +233,7 @@
@@ -233,7 +233,7 @@
<script src="/js/vibes.js?v=10"></script>
<script src="/js/vibes.js?v=10"></script>
<script src="/js/opencoins.js?v=6"></script>
<script src="/js/opencoins.js?v=6"></script>
<script src="/js/cosmetics-ui.js?v=2"></script>
<script src="/js/cosmetics-ui.js?v=2"></script>
- <script src="/js/chat.js?v=75"></script>
+ <script src="/js/chat.js?v=76"></script>
<script src="/js/chat-autocomplete.js?v=1"></script>
<script src="/js/chat-autocomplete.js?v=1"></script>
<script>
<script>
'use strict';
'use strict';
@@ -1638,6 +1638,19 @@ class ChatServer {
@@ -1638,6 +1638,19 @@ class ChatServer {
async synthesizeAndBroadcastTTS(streamId, username, text, voiceFX, sourcePlatform = null, identityKey = null, channelUserId = null) {
async synthesizeAndBroadcastTTS(streamId, username, text, voiceFX, sourcePlatform = null, identityKey = null, channelUserId = null) {
// Queue accounting key: per-stream when live, per-channel when offline.
// Queue accounting key: per-stream when live, per-channel when offline.
const queueKey = streamId || (channelUserId ? `ch:${channelUserId}` : null);
const queueKey = streamId || (channelUserId ? `ch:${channelUserId}` : null);
+ // Same utterance, same room, twice within a few seconds = a duplicated bridge
+ // or double-fired relay (RS restart races have produced exactly this) — one
+ // synthesis, one broadcast. Keyed per room so multi-slot mirrors still get
+ // their own copy for their own viewers.
+ if (!this._recentTtsDedupe) this._recentTtsDedupe = new Map();
+ const dedupeKey = `${queueKey}|${identityKey || username}|${String(text).slice(0, 200)}`;
+ const nowMs = Date.now();
+ const lastMs = this._recentTtsDedupe.get(dedupeKey);
+ if (lastMs && nowMs - lastMs < 8000) { console.log(`[TTS] deduped duplicate synth for ${username} in ${queueKey}`); return; }
+ this._recentTtsDedupe.set(dedupeKey, nowMs);
+ if (this._recentTtsDedupe.size > 500) {
+ for (const [k, t] of this._recentTtsDedupe) if (nowMs - t > 30000) this._recentTtsDedupe.delete(k);
+ }
try {
try {
// "." prefix = user opted this message out of TTS — never synthesize or broadcast it.
// "." prefix = user opted this message out of TTS — never synthesize or broadcast it.
if (String(text || '').trimStart().startsWith('.')) return;
if (String(text || '').trimStart().startsWith('.')) return;