openvibers/openvibe.live Commits

PowerChat overlay: AI chat viewers relay too, with proper placeholder avatars

OP OpenVibers committed 27 Aug 2026 · c33b8c20fe · 4 files, +27 −2

AI viewers inject chat via broadcastToStream directly — never through
handleChatMessage where the PowerChat relay lives — so human chatters
showed on the overlay while every bot line was invisible. The injector
now forwards to PowerChat (channel-relay-gated, keyed by the persisted
message id, TTS key included) with a 🤖 avatarFallback. forwardChat
gains avatarFallback support, and the RS/platform relays use it to send
the REAL name's first letter — "[RS] name"-style prefixes rendered "["
as the placeholder for every relayed chatter.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017AyeSFu7qdLdR64Zv99QNm

Changes

ddiff --git a/server/ai/viewers/engine.js b/server/ai/viewers/engine.js
iindex 85d25119..c0f8ec8e 100644
---- a/server/ai/viewers/engine.js
++++ b/server/ai/viewers/engine.js
@@@ -266,9 +266,25 @@ class AiViewersEngine {
const chatServer = require('../../chat/chat-server');
chatServer.broadcastToStream(streamId, chatMsg);
chatServer.forwardToGlobal(streamId, chatMsg);
-- chatServer.synthesizeAndBroadcastTTS(streamId, bot.username, message, null, null, `aibot:${bot.username.toLowerCase()}`);
++ chatServer.synthesizeAndBroadcastTTS(streamId, bot.username, message, null, null, `aibot:${bot.username.toLowerCase()}`, null, id ? `m${id}` : null);
} catch (e) { console.warn('[AI-Viewers] broadcast failed:', e.message); }
++ // ...and into the streamer's PowerChat unified overlay. AI viewers inject here
++ // directly (never through handleChatMessage where the normal relay lives), so
++ // they were invisible on PowerChat while every human chatter showed up.
++ try {
++ const pc = require('../../integrations/powerchat-platform');
++ if (worker.userId && pc.channelRelayEnabled(worker.userId, streamId)) {
++ pc.forwardChat(worker.userId, {
++ chatterName: bot.username,
++ externalChatterId: `ai:${bot.username.toLowerCase()}`,
++ message,
++ messageId: id ? `ov-${id}` : undefined,
++ avatarFallback: '🤖',
++ });
++ }
++ } catch { /* non-critical */ }
++
worker.lastPostAt = Date.now();
worker.lastPoster = bot.id;
worker.nextGapMs = this._nextGap((Date.now() - worker.lastRealInputAt) < ACTIVE_WINDOW_MS);
ddiff --git a/server/integrations/chat-relay-service.js b/server/integrations/chat-relay-service.js
iindex b82fa5d3..ee8346ac 100644
---- a/server/integrations/chat-relay-service.js
++++ b/server/integrations/chat-relay-service.js
@@@ -446,6 +446,9 @@ class ChatRelayService {
message: chatMsg.message,
messageId: chatMsg.id ? `ov-${chatMsg.id}` : undefined,
avatarUrl: extras.avatar_url || undefined,
++ // Placeholder letter from the REAL name — "[Twitch] name" would
++ // otherwise render "[" for every relayed chatter.
++ avatarFallback: [...String(username || '')][0] || undefined,
});
}
} catch { /* non-critical */ }
ddiff --git a/server/integrations/powerchat-platform.js b/server/integrations/powerchat-platform.js
iindex b898aa84..3d94baa7 100644
---- a/server/integrations/powerchat-platform.js
++++ b/server/integrations/powerchat-platform.js
@@@ -145,7 +145,7 @@ function destRelayEnabled(destId) {
// ── chat:write ───────────────────────────────────────────────
const _chatBuckets = new Map(); // userId → { count, resetAt } (~120/min limit; we cap at 100)
--async function forwardChat(streamerUserId, { chatterName, externalChatterId, message, messageId, avatarUrl, isModerator, isSubscriber } = {}) {
++async function forwardChat(streamerUserId, { chatterName, externalChatterId, message, messageId, avatarUrl, avatarFallback, isModerator, isSubscriber } = {}) {
if (!message || !chatterName) return;
const conn = _connFor(streamerUserId, 'chat:write');
if (!conn) return;
@@@ -169,6 +169,9 @@ async function forwardChat(streamerUserId, { chatterName, externalChatterId, mes
message: String(message).slice(0, 500),
messageId: String(messageId || crypto.randomUUID()).slice(0, 128),
...(absAvatar ? { avatarUrl: absAvatar } : {}),
++ // Placeholder grapheme when no avatar URL resolves (e.g. 🤖 for AI
++ // viewers, or a clean letter for "[RS] name"-style prefixed sources).
++ ...(avatarFallback ? { avatarFallback: String(avatarFallback).slice(0, 8) } : {}),
...(isModerator ? { isModerator: true } : {}),
...(isSubscriber ? { isSubscriber: true } : {}),
},
ddiff --git a/server/integrations/robotstreamer-service.js b/server/integrations/robotstreamer-service.js
iindex 9a49497e..3a586dc0 100644
---- a/server/integrations/robotstreamer-service.js
++++ b/server/integrations/robotstreamer-service.js
@@@ -753,6 +753,9 @@ class RobotStreamerService {
message: mirrored.message,
messageId: mirrored.id ? `ov-${mirrored.id}` : undefined,
avatarUrl: data.avatar || undefined,
++ // Placeholder letter from the REAL name — "[RS] name"
++ // would otherwise render "[" for every RS chatter.
++ avatarFallback: [...String(rawUsername || '')][0] || undefined,
});
}
} catch { /* non-critical */ }