From 43ac28bb47ce0553878f7b3436b77b57e643690b Mon Sep 17 00:00:00 2001 From: Rodrigo Emanuel Date: Tue, 6 May 2025 20:32:33 -0300 Subject: [PATCH] Add files via upload --- README.md | 36 +++++++++++++++++++--------- chat.html | 5 +--- css/settings.css | 10 ++++++++ index.html | 28 +++++++++++++++++++--- js/app.js | 47 +++++++++++++++++++++++++++++++++++-- js/lang/en.js | 1 + js/lang/es.js | 1 + js/lang/ptbr.js | 1 + js/settings.js | 15 ++++++------ js/twitch/module.js | 1 - js/youtube/module.js | 6 ++--- streamerbot-import.vortisrd | 2 +- 12 files changed, 120 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 698e09e..bc66700 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,9 @@ ChatRD is a chat overlay widget for OBS that unifies messages and events from ** ## 🛠️ Usage -1. Make sure your **Twitch** and **YouTube** accounts are connected on **Streamer.bot** and you have **TikFinity Desktop App** installed and set up to your account on **TikTok**. **BOTH APPS NEED TO RUN ON THE SAME PC**. -2. In **Streamer.bot**, go to **Server/Clients → WebSocket Server** and make sure it is running -3. Import the string inside the file [streamerbot-import.vortisrd](https://github.com/vortisrd/chatrd/blob/main/streamerbot-import.vortisrd) to your **Streamer.bot** using the **Import** button at the top. +1. Make sure your **Twitch** and **YouTube** accounts are connected on **Streamer.Bot** and you have **TikFinity Desktop App** installed and set up to your account on **TikTok**. **BOTH APPS NEED TO RUN ON THE SAME PC**. +2. In **Streamer.Bot**, go to **Server/Clients → WebSocket Server** and make sure it is running +3. Import the string inside the file [streamerbot-import.vortisrd](https://github.com/vortisrd/chatrd/blob/main/streamerbot-import.vortisrd) to your **Streamer.Bot** using the **Import** button at the top. 4. Open the [Settings Page](https://vortisrd.github.io/chatrd) in your browser 5. Choose your desired options 6. Click **"Copy URL"** @@ -30,21 +30,34 @@ ChatRD is a chat overlay widget for OBS that unifies messages and events from ** ## 🛠️ How to Use it in a Local Network -1. On **Streamer.bot**, go to **Server/Clients → WebSocket Server** and make sure the Address is set to the Local Network IP from the PC, for example **192.168.0.10** ... or, if you prefer, use 0.0.0.0. -2. On the [Settings Page](https://vortisrd.github.io/chatrd), make sure you set that IP on **Streamer.bot WebSocket Server**. +1. On **Streamer.Bot**, go to **Server/Clients → WebSocket Server** and make sure the Address is set to the Local Network IP from the PC, for example **192.168.0.10** ... or, if you prefer, use 0.0.0.0. +2. On the [Settings Page](https://vortisrd.github.io/chatrd), make sure you set that IP on **Streamer.Bot WebSocket Server**. 3. Turn the **"Run Locally"** switch on. 4. Click **"Copy URL"** 5. [Download ChatRD](https://github.com/vortisrd/chatrd/archive/refs/heads/main.zip) on the machine you want to see the chat on or want to set up OBS in, unzipping the file. 6. Open it on your browser or add it in OBS as Browser Source (don't tick **Local File**)following the file trail, for example: **file:///C:/PATH_TO_THE_FILE/chat.html**. 7. After **chat.html**, paste the URL copied from the configurator. The full link should be like this: **file:///C:/PATH_TO_THE_FILE/chat.html?language=ptbr&showPlatform=true&showAvatar=true&showTimestamps=false&showBadges=true&showPlatformStatistics=false&excludeCommands=true&showTwitchMessages=true&showTwitchFollows=true&showTwitchBits=true&showTwitchAnnouncements=true&showTwitchSubs=true&showTwitchGiftedSubs=true&showTwitchMassGiftedSubs=true&showTwitchGiftedSubsUserTrain=true&showTwitchRewardRedemptions=true&showTwitchRaids=true&showTwitchSharedChat=true&showTwitchViewers=true&showYouTubeMessages=true&showYouTubeSuperChats=true&showYouTubeSuperStickers=false&showYouTubeMemberships=true&showYouTubeGiftMemberships=true&showYouTubeMembershipsTrain=true&showYouTubeStatistics=true&showTikTokMessages=true&showTikTokFollows=true&showTikTokGifts=true&showTikTokSubs=true&showTikTokStatistics=true&showStreamlabsDonations=true&showStreamElementsTips=true&ignoreChatters=&streamerBotServerAddress=127.0.0.1&streamerBotServerPort=8080&=&hideAfter=0** +--- + +## 🔊 How to set TTS with Speaker.Bot + +### Speaker.Bot Setup +1. Go to **Settings → WebSocket Server**, click on *Start Server*. Make sure to also tick the *Auto-Start* checkbox. +2. Go to **Settings → Speech Engine** and add the TTS Service of your preference. (Sapi5 is the Windows default). +3. Go to **Settings → Voice Aliases**, name the voice *SpeakerBot* and click **Add** right next to it. +4. In the Left Column, click on the **SpeakerBot** you just added and on the **Speak!** section, select the voice you want to use and click **Add**. (If you're using Sapi5, I recommend using *Microsoft Zira Desktop* as a voice). + +### Streamer.Bot Setup +1. Import the [streamerbot-import.vortisrd](https://github.com/vortisrd/chatrd/blob/main/streamerbot-import.vortisrd) file to your **Streamer.Bot**. There's a new action that will handle the **Speaker.Bot** integration. +2. Go to **Integrations → Speaker.Bot**, click on *Connect*. Make sure to also tick the *Auto-Start* and *Auto-Connect* checkboxes. --- ## 🧩 Integrations -- 🟣 **Twitch** (via Streamer.bot) -- 🔴 **YouTube** (via Streamer.bot) +- 🟣 **Twitch** (via Streamer.Bot) +- 🔴 **YouTube** (via Streamer.Bot) - ⚫ **TikTok** (via TikFinity Desktop App) - 💸 **Streamlabs / StreamElements** @@ -52,8 +65,9 @@ ChatRD is a chat overlay widget for OBS that unifies messages and events from ** ## 📦 Dependencies -- [Streamer.bot](https://streamer.bot) -- [Streamer.bot Client JS](https://streamerbot.github.io/client/) +- [Streamer.Bot](https://streamer.bot) +- [Speaker.Bot](http://speaker.bot/) +- [Streamer.Bot Client JS](https://streamerbot.github.io/client/) - [TikFinity Desktop App](https://tikfinity.zerody.one/) - [Font Awesome](https://fontawesome.com/) - [Animate.css](https://animate.style/) @@ -78,9 +92,9 @@ ChatRD is a chat overlay widget for OBS that unifies messages and events from ** ## **⚠️ DISCLAIMERS ⚠️** ### About YouTube Membership Emojis -I tried to add member emotes but **that is currently impossible due to YouTube's API not exposing Members Emotes and with that, Streamer.bot won't be able to show them.**. So I've added a way for the users to add them manually at the overlay, with the data saved as a Streamer.Bot Global Variable. +I tried to add member emotes but **that is currently impossible due to YouTube's API not exposing Members Emotes and with that, Streamer.Bot won't be able to show them.**. So I've added a way for the users to add them manually at the overlay, with the data saved as a Streamer.Bot Global Variable. -What Casterlabs Caffeinated, Social Stream Ninja and Onecomme do to scrape the emotes won't work with the current way Streamer.bot and my code works, so I had to choose between **making the user add them manually** or build a **server-sided executable (using NodeJS, Python or whatever) to read the chat as it's going or scrape the HTML code**. I don't want to add another executable on top of the user's flow, so it would be easier to use what it's currently available. **And no, I won't do any research based on what other tools do.** Tried to do that and wasted 1 week of my life doing it. +What Casterlabs Caffeinated, Social Stream Ninja and Onecomme do to scrape the emotes won't work with the current way Streamer.Bot and my code works, so I had to choose between **making the user add them manually** or build a **server-sided executable (using NodeJS, Python or whatever) to read the chat as it's going or scrape the HTML code**. I don't want to add another executable on top of the user's flow, so it would be easier to use what it's currently available. **And no, I won't do any research based on what other tools do.** Tried to do that and wasted 1 week of my life doing it. When YouTube decide to expose their Partner Emotes on their API, I'll come back to this. diff --git a/chat.html b/chat.html index 46ab4c5..0c065de 100644 --- a/chat.html +++ b/chat.html @@ -45,13 +45,10 @@ - - - @@ -69,4 +66,4 @@ - + \ No newline at end of file diff --git a/css/settings.css b/css/settings.css index 55b94b3..8f521b6 100644 --- a/css/settings.css +++ b/css/settings.css @@ -422,3 +422,13 @@ footer a { justify-content: flex-end; gap: 1em; } + + +#memberemotesbstatus.online i { + color: #00dd63; + text-shadow: 0 0 5px #00dd63; +} +#memberemotesbstatus.offline i { + color: #ff0000; + text-shadow: 0 0 5px #ff0000; +} \ No newline at end of file diff --git a/index.html b/index.html index 687bb47..8317aad 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + ChatRD - Config @@ -22,9 +22,23 @@

Config

+ +
+ + + Streamer.Bot is Offline! + +
+
-
+
+ +
+ +
Speaker.Bot is required for Text-to-Speech! Follow the tutorial.
+ +
@@ -89,8 +103,16 @@
+ + +
- +
diff --git a/js/app.js b/js/app.js index e3f5057..1e1efeb 100644 --- a/js/app.js +++ b/js/app.js @@ -4,6 +4,10 @@ const streamerBotServerAddress = getURLParam("streamerBotServerAddress", "127.0.0.1"); const streamerBotServerPort = getURLParam("streamerBotServerPort", "8080"); + +const ttsSpeakerBotChat = getURLParam("ttsSpeakerBotChat", false); +const ttsSpeakerBotEvents = getURLParam("ttsSpeakerBotEvents", false); + let streamerBotConnected = false; const chatThreshhold = 50; @@ -75,6 +79,8 @@ const streamerBotClient = new StreamerbotClient({ async function addMessageToChat(userID, messageID, platform, data) { + + if (ttsSpeakerBotChat == true) { ttsSpeakerBotSays(data.userName, currentLang.ttschat, data.message); } const html = DOMPurify.sanitize(`
@@ -121,6 +127,8 @@ async function addMessageToChat(userID, messageID, platform, data) { async function addEventToChat(userID, messageID, platform, data) { + if (ttsSpeakerBotEvents == true) { ttsSpeakerBotSays(data.userName, '', data.message); } + const html = DOMPurify.sanitize(`
@@ -149,7 +157,7 @@ async function addEventToChat(userID, messageID, platform, data) { }, 1000); }, Math.floor(hideAfter * 1000)); } - + removeExtraChatMessages(); } @@ -298,7 +306,42 @@ const notifySuccess = (success) => { } - function escapeRegex(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + + +async function ttsSpeakerBotSays(user, action, message) { + + if (streamerBotConnected == false) return; + + const ttstext = await cleanStringOfHTMLButEmotes(message); + const ttsmessage = user+' '+action+' '+ttstext; + + streamerBotClient.doAction( + { name : "TTS Event" }, + { + "ttsmessage": ttsmessage, + } + ).then( (ttsstuff) => { + console.debug('Sending TTS to Streamer.Bot', ttsstuff); + }); +} + + +async function cleanStringOfHTMLButEmotes(string) { + // Cria um elemento DOM temporário + const container = document.createElement('div'); + container.innerHTML = string; + + // Substitui ... por texto do alt + const emotes = container.querySelectorAll('img.emote[alt]'); + emotes.forEach(img => { + const altText = img.getAttribute('alt'); + const textNode = document.createTextNode(altText); + img.replaceWith(textNode); + }); + + // Remove todo o restante do HTML + return container.textContent || ""; } \ No newline at end of file diff --git a/js/lang/en.js b/js/lang/en.js index be4d27f..9282062 100644 --- a/js/lang/en.js +++ b/js/lang/en.js @@ -1,6 +1,7 @@ const en = { streamerbotconnected: 'Streamer.bot Online!', streamerbotdisconnected: 'Streamer.bot Disconnected!', + ttschat: 'said', twitch : { firstMessage : () => `First chatter`, diff --git a/js/lang/es.js b/js/lang/es.js index 549aeb7..1d382dc 100644 --- a/js/lang/es.js +++ b/js/lang/es.js @@ -1,6 +1,7 @@ const es = { streamerbotconnected: '¡Streamer.bot en línea!', streamerbotdisconnected: '¡Streamer.bot desconectado!', + ttschat: 'dijo', twitch : { firstMessage : () => `Primeira mensaje`, diff --git a/js/lang/ptbr.js b/js/lang/ptbr.js index 318028f..2b31303 100644 --- a/js/lang/ptbr.js +++ b/js/lang/ptbr.js @@ -1,6 +1,7 @@ const ptbr = { streamerbotconnected: 'Streamer.bot Conectado!', streamerbotdisconnected: 'Streamer.bot Desconectado!', + ttschat: 'disse', twitch : { firstMessage : () => `Primeira mensagem`, diff --git a/js/settings.js b/js/settings.js index 200baf2..e8529e6 100644 --- a/js/settings.js +++ b/js/settings.js @@ -77,10 +77,9 @@ async function loadSettingsFromLocalStorage() { onConnect: (data) => { streamerBotConnected = true; - var sbstatus = document.getElementById('memberemotesbstatus'); - - sbstatus.style.color = '#00dd63'; - sbstatus.textContent = 'Streamer.Bot is Online!'; + document.querySelector('#memberemotesbstatus').classList.remove('offline'); + document.querySelector('#memberemotesbstatus').classList.add('online'); + document.querySelector('#memberemotesbstatus span').textContent = 'Streamer.Bot is Online!'; streamerBotClient.getGlobals().then( (getglobals) => { const settings = JSON.parse(getglobals.variables.chatrdytcustomemotes.value); @@ -96,10 +95,10 @@ async function loadSettingsFromLocalStorage() { console.error('Streamer.bot Disconnected!'); streamerBotConnected = false; - - var sbstatus = document.getElementById('memberemotesbstatus'); - sbstatus.style.color = '#ff0000'; - sbstatus.textContent = 'Streamer.Bot Needs to be Online!'; + + document.querySelector('#memberemotesbstatus').classList.remove('online'); + document.querySelector('#memberemotesbstatus').classList.add('offline'); + document.querySelector('#memberemotesbstatus span').textContent = 'Streamer.Bot is Offline!'; } }); diff --git a/js/twitch/module.js b/js/twitch/module.js index 0f2c05b..c9cf606 100644 --- a/js/twitch/module.js +++ b/js/twitch/module.js @@ -515,7 +515,6 @@ async function twitchChatClearMessages() { async function twitchUpdateStatistics(data) { - if (showPlatformStatistics == false || showTwitchViewers == false) return; const viewers = DOMPurify.sanitize(data.viewerCount); diff --git a/js/youtube/module.js b/js/youtube/module.js index 6a0e147..1566592 100644 --- a/js/youtube/module.js +++ b/js/youtube/module.js @@ -353,7 +353,7 @@ async function getYouTubeEmotes(data) { const emoteRegex = new RegExp(`(?`; + const emoteElement = `${escapedCode}`; message = message.replace(emoteRegex, emoteElement); } @@ -362,7 +362,7 @@ async function getYouTubeEmotes(data) { if (data.emotes) { for (const emote of data.emotes) { const emoteRegex = new RegExp(escapeRegex(emote.name), 'g'); - const emoteElement = ``; + const emoteElement = `${emote.name}`; message = message.replace(emoteRegex, emoteElement); } } @@ -372,7 +372,7 @@ async function getYouTubeEmotes(data) { if (data.user.isSponsor == true || data.user.isOwner == true) { message = message.replace(/:([a-zA-Z0-9_]+):/g, (match, emoteName) => { if (youTubeCustomEmotes[emoteName]) { - return ``; + return `${emoteName}`; } return match; }); diff --git a/streamerbot-import.vortisrd b/streamerbot-import.vortisrd index 819b149..a4380d5 100644 --- a/streamerbot-import.vortisrd +++ b/streamerbot-import.vortisrd @@ -1 +1 @@ -U0JBRR+LCAAAAAAABADVV1lz4kYQfk9V/gPFc8TqMkKu2gcjAwbvsgZzyWEf5pKsMNIomhFY3vJ/z0gcRgg2m31JQhUlqb+e7p4+Z779+kutVg+JAPXr2rf8Q35GICTys+48AzG+rf+2I4NUPLMkB2YsEQE/gtYk4QGLckxtqA3jAGDCURLEYgcey2LjNLpBOyRKKd1jYRAFYRrODjJzMMfeCo46BiVrQSGDS8rvW0ptDxVwgHPF2PO8poEsBZiopZjQtBSo4ZaCPNswNY8Qr2XvjSuW/ZmSlJQNK+gkApCSXKZIUlJCXhBNMekmLLwLuGBJJpk8QPklrgcS4SDyz3HtIzAJVhO2qnXWJBK8ZJ+fsDSuBmnrEboBGZfuPSc6ARFm4cHxFRyxCKVJIhWe2aNIAt+XcTl29onDt0JoIAX0C9cbNiEAaEhpAk3LXe8ptm5jBVpm00bY0mATHJt/FLUrA9jEAqbSagEZNeNKxo/ApkJaFtItzzIg1CpLRRbnvmupV6fIxdi9R4bvU+nrMfr2/vG15Ohq6p3zxj6aFVMvVMd7rIhHZCQQqagoYOd6uZwHMpwbvlx+DlDCOPNEY9iZLJfdRCrdsGTVNJfLtZkXpWpo9nIZcsQSGsAGprReFvn1VD/MBHEYLozHi2EMQ+RPDfqKezPxZaPe72mTcGbgnp0i3Q6xc3Uvn+mn1UsMo451O2JDJ2prbvgSu1n7D9jrvqKsfTvtPA+gpMFwKnE+dIIbv++0N3g+4GD+2XdDew2ddpf0Zn/gxZjeO6s9Ty5TPm+2/7uhikKaPmVt8bQYvrpzTPu3qu8uBhHSeADns1ekd6OnxwH+RMfrqTHOwPwqkvLKcjrxgxvG1DVGPoiGaxjc2Ee0dNrrZkifsnd5J+tv1PvDu9PW3UXfR6HkLfwy9R8e2+nTAvmPKzsAodyT5HdGNJcxkE/Yj9rcXYxDaAxEn6qS/yaY9Wgy69lJP9gMHX81cGY8eOp1VVfuJd9jYadfyLHzeDij1UDqL3hHWrv/iWLqSh5XFxR3BhnUuytX777iTvwKdZMd2ZdKPRN8N8jjye63e9v5e/uXvCpeDNL+3TjD8+lh795IvfdGHz9WkjdOCGJhHFByps3s0p+C7FGARFzi4GBNxoSnVEzYDCRBXr/f4y1xVetp21VsqwWsVlNVMLJkV/FMIrsKNpQr02x6GHhm0zQrSzck8J9zO+V8u9Bx7Px3iu379MkcKbDv9yNZ2OQlV/jjnaiXK9u2r3JLpxTEnOAjfA+/HRirQ9ODEJmejhTZo03FNC3ZiAmxFehhjDzLQwa0/oND02XpJIWk5qRSYFjrhEyQf2N4VtDj6fkzc0SOCxFEYKe8kobrXeoPd35AcnMJzgQq/EAqbigWxfkpi4uLWchZmiByXh1Nf1zPNqM0CxPLk7Mc6oalmFgl8iwgp3rTMkCrBZHlEftnSk/T9f9d4W1f9vzb2imJkMvDUCZcmbghkDO0IuKRJOuTZHoHneIA9jeH4n96Mjs+lnblIV1k9XKV45gFRerXN/z6wwdNtxr5dUC71jVdMz6UuAXlDwkTTPqIn6RXcTtwWBQRdLaO8sFSgF+i24CjA+Npc0mISLJ+JKSjAJW4UVISEs6BT0a7nlVouRQcEYR7Vx9dRN5vPZq2pZCXWF6NCM471fYypDda221XrzUFaipQXr4aTXkUe/sLjCjDvI0NAAA= \ No newline at end of file +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 \ No newline at end of file