Skip to content
60 changes: 60 additions & 0 deletions src/plugins/betterTTS/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# BetterTTS for Vencord

This is a porting of the original BetterDiscord(BD) plugin [BetterTTS](https://github.com/nicola02nb/BetterDiscord-Stuff/tree/main/Plugins/BetterTTS).

A Vencord(VC) plugin that allows you to play a custom TTS when a message is received.

## Features:

- Enable/Disable `/tts` command
- Enable/Disable Announce Client join/left channel
- Enable/Disable Read Messages from channels when recieved

### Mesage Reading:

- Prepend/Not Prepend Server name before reading messages
- Prepend/Not Prepend Channel name before reading messages
- Prepend/Not Prepend Usernames before reading messages
- Set which name should be read for users
- Set how URLs should be read
- Select from which channel TTS should read messages:
- Never read messages
- Read all messages from all Channels and Servers
- From Custom subscribed Channels or Servers
- From Connected Channel
- From Focused Channel
- From Connected Server
- From Focused Server
- Subscribe/Unsubscribe form servers(guilds) and channels (There is a checkbox when you right click them)

## TTS Sources:

You can select:

- The TTS Audio Source:
- The Voice Type and Languages

Sources Available:

- Discord Default TTS https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis
- Streamelements API (About 206 Voices) https://api.streamelements.com/kappa/v2/speech
- Some TikTok voices https://tiktok-tts.weilnet.workers.dev/api/generation

### Block Filters:

Block messages from:

- Blocked users
- Ignored users
- Not firends users
- Muted channels
- Muted servers
- Muted users (There is a checkbox when you right click them)

### Other

- Adjust Volume
- Select Speech Rate
- Play an audio preview
- Select Delay between Messages
- Set a Keyboard Shortcut to Toggle TTS On/Off (With toast)
37 changes: 37 additions & 0 deletions src/plugins/betterTTS/Sources/AbstractSource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/

import { TTSSourceInterface, TTSVoice, TTSVoiceOption } from "../types/ttssource";

export abstract class AbstractTTSSource<T extends HTMLAudioElement | SpeechSynthesisUtterance> implements TTSSourceInterface<T> {
constructor() {
const retrive = async () => {
console.log("Retrieving voices...");
this.voicesLabels = await this.retrieveVoices();
if (this.voicesLabels.length === 0) {
setTimeout(retrive, 5000);
}
};
retrive();
}

protected voicesLabels: TTSVoiceOption[] = [];
protected selectedVoice: TTSVoice = "";

abstract retrieveVoices(): Promise<TTSVoiceOption[]>;

abstract getDefaultVoice(): TTSVoice;

getVoices(): TTSVoiceOption[] {
return this.voicesLabels;
}

setVoice(voice: TTSVoice): void {
this.selectedVoice = voice;
}

abstract getMedia(text: string): Promise<T>;
}
31 changes: 31 additions & 0 deletions src/plugins/betterTTS/Sources/Discord.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/

import { AbstractTTSSource } from "./AbstractSource";

export default new class DiscordTTS extends AbstractTTSSource<SpeechSynthesisUtterance> {

getDefaultVoice() {
return speechSynthesis.getVoices()[0].voiceURI;
}

async retrieveVoices() {
const voices = speechSynthesis.getVoices();
this.voicesLabels = voices
.sort((a, b) => a.name.localeCompare(b.name))
.map(voice => ({
label: voice.name,
value: voice.voiceURI
}));
return this.voicesLabels;
}

async getMedia(text: string) {
const utterance = new SpeechSynthesisUtterance(text);
utterance.voice = speechSynthesis.getVoices().find(v => v.voiceURI === this.selectedVoice) || speechSynthesis.getVoices()[0];
return utterance;
}
};
52 changes: 52 additions & 0 deletions src/plugins/betterTTS/Sources/Streamelements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/

import { AbstractTTSSource } from "./AbstractSource";

type StreamElementsVoice = {
gender: string;
id: string;
languageName: string;
languageCode: string;
name: string;
provider: string;
};

export default new class StreamElementsTTS extends AbstractTTSSource<HTMLAudioElement> {
getDefaultVoice() {
return "Brian";
}

async retrieveVoices() {
try {
const response = await fetch("https://api.streamelements.com/kappa/v2/speech/voices");
if (!response.ok) {
throw new Error("Failed to load voices");
}
const data = await response.json();
const voices = data.voices as Record<string, StreamElementsVoice>;
this.voicesLabels = Object.values(voices)
.sort((a, b) => a.languageName.localeCompare(b.languageName))
.map((voice: StreamElementsVoice) => ({
label: `${voice.name} (${voice.languageName} ${voice.languageCode})`,
value: voice.id
}));
} catch (error) {
throw new Error("Failed to load voices");
}
return this.voicesLabels;
}

async getMedia(text: string) {
return new Promise<HTMLAudioElement>((resolve, reject) => {
text = encodeURIComponent(text);
const url = `https://api.streamelements.com/kappa/v2/speech?voice=${this.selectedVoice}&text=${text}`;
const audio = new Audio(url);
audio.addEventListener("loadeddata", () => resolve(audio));
audio.addEventListener("error", () => reject(new Error("Failed to load audio")));
});
}
};
134 changes: 134 additions & 0 deletions src/plugins/betterTTS/Sources/TikTok.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/

import { AbstractTTSSource } from "./AbstractSource";

export default new class TikTokTTS extends AbstractTTSSource<HTMLAudioElement> {
getDefaultVoice() {
return "en_us_001";
}

async retrieveVoices() {
this.voicesLabels = [
{ label: "GHOSTFACE", value: "en_us_ghostface" },
{ label: "CHEWBACCA", value: "en_us_chewbacca" },
{ label: "C3PO", value: "en_us_c3po" },
{ label: "STITCH", value: "en_us_stitch" },
{ label: "STORMTROOPER", value: "en_us_stormtrooper" },
{ label: "ROCKET", value: "en_us_rocket" },
{ label: "MADAME_LEOTA", value: "en_female_madam_leota" },
{ label: "GHOST_HOST", value: "en_male_ghosthost" },
{ label: "PIRATE", value: "en_male_pirate" },
{ label: "AU_FEMALE_1", value: "en_au_001" },
{ label: "AU_MALE_1", value: "en_au_002" },
{ label: "UK_MALE_1", value: "en_uk_001" },
{ label: "UK_MALE_2", value: "en_uk_003" },
{ label: "US_FEMALE_1", value: "en_us_001" },
{ label: "US_FEMALE_2", value: "en_us_002" },
{ label: "US_MALE_1", value: "en_us_006" },
{ label: "US_MALE_2", value: "en_us_007" },
{ label: "US_MALE_3", value: "en_us_009" },
{ label: "US_MALE_4", value: "en_us_010" },
{ label: "MALE_JOMBOY", value: "en_male_jomboy" },
{ label: "MALE_CODY", value: "en_male_cody" },
{ label: "FEMALE_SAMC", value: "en_female_samc" },
{ label: "FEMALE_MAKEUP", value: "en_female_makeup" },
{ label: "FEMALE_RICHGIRL", value: "en_female_richgirl" },
{ label: "MALE_GRINCH", value: "en_male_grinch" },
{ label: "MALE_DEADPOOL", value: "en_male_deadpool" },
{ label: "MALE_JARVIS", value: "en_male_jarvis" },
{ label: "MALE_ASHMAGIC", value: "en_male_ashmagic" },
{ label: "MALE_OLANTERKKERS", value: "en_male_olantekkers" },
{ label: "MALE_UKNEIGHBOR", value: "en_male_ukneighbor" },
{ label: "MALE_UKBUTLER", value: "en_male_ukbutler" },
{ label: "FEMALE_SHENNA", value: "en_female_shenna" },
{ label: "FEMALE_PANSINO", value: "en_female_pansino" },
{ label: "MALE_TREVOR", value: "en_male_trevor" },
{ label: "FEMALE_BETTY", value: "en_female_betty" },
{ label: "MALE_CUPID", value: "en_male_cupid" },
{ label: "FEMALE_GRANDMA", value: "en_female_grandma" },
{ label: "MALE_XMXS_CHRISTMAS", value: "en_male_m2_xhxs_m03_christmas" },
{ label: "MALE_SANTA_NARRATION", value: "en_male_santa_narration" },
{ label: "MALE_SING_DEEP_JINGLE", value: "en_male_sing_deep_jingle" },
{ label: "MALE_SANTA_EFFECT", value: "en_male_santa_effect" },
{ label: "FEMALE_HT_NEYEAR", value: "en_female_ht_f08_newyear" },
{ label: "MALE_WIZARD", value: "en_male_wizard" },
{ label: "FEMALE_HT_HALLOWEEN", value: "en_female_ht_f08_halloween" },
{ label: "FR_MALE_1", value: "fr_001" },
{ label: "FR_MALE_2", value: "fr_002" },
{ label: "DE_FEMALE", value: "de_001" },
{ label: "DE_MALE", value: "de_002" },
{ label: "ES_MALE", value: "es_002" },
{ label: "ES_MX_MALE", value: "es_mx_002" },
{ label: "BR_FEMALE_1", value: "br_001" },
{ label: "BR_FEMALE_2", value: "br_003" },
{ label: "BR_FEMALE_3", value: "br_004" },
{ label: "BR_MALE", value: "br_005" },
{ label: "BP_FEMALE_IVETE", value: "bp_female_ivete" },
{ label: "BP_FEMALE_LUDMILLA", value: "bp_female_ludmilla" },
{ label: "PT_FEMALE_LHAYS", value: "pt_female_lhays" },
{ label: "PT_FEMALE_LAIZZA", value: "pt_female_laizza" },
{ label: "PT_MALE_BUENO", value: "pt_male_bueno" },
{ label: "ID_FEMALE", value: "id_001" },
{ label: "JP_FEMALE_1", value: "jp_001" },
{ label: "JP_FEMALE_2", value: "jp_003" },
{ label: "JP_FEMALE_3", value: "jp_005" },
{ label: "JP_MALE", value: "jp_006" },
{ label: "KR_MALE_1", value: "kr_002" },
{ label: "KR_FEMALE", value: "kr_003" },
{ label: "KR_MALE_2", value: "kr_004" },
{ label: "JP_FEMALE_FUJICOCHAN", value: "jp_female_fujicochan" },
{ label: "JP_FEMALE_HASEGAWARIONA", value: "jp_female_hasegawariona" },
{ label: "JP_MALE_KEIICHINAKANO", value: "jp_male_keiichinakano" },
{ label: "JP_FEMALE_OOMAEAIIKA", value: "jp_female_oomaeaika" },
{ label: "JP_MALE_YUJINCHIGUSA", value: "jp_male_yujinchigusa" },
{ label: "JP_FEMALE_SHIROU", value: "jp_female_shirou" },
{ label: "JP_MALE_TAMAWAKAZUKI", value: "jp_male_tamawakazuki" },
{ label: "JP_FEMALE_KAORISHOJI", value: "jp_female_kaorishoji" },
{ label: "JP_FEMALE_YAGISHAKI", value: "jp_female_yagishaki" },
{ label: "JP_MALE_HIKAKIN", value: "jp_male_hikakin" },
{ label: "JP_FEMALE_REI", value: "jp_female_rei" },
{ label: "JP_MALE_SHUICHIRO", value: "jp_male_shuichiro" },
{ label: "JP_MALE_MATSUDAKE", value: "jp_male_matsudake" },
{ label: "JP_FEMALE_MACHIKORIIITA", value: "jp_female_machikoriiita" },
{ label: "JP_MALE_MATSUO", value: "jp_male_matsuo" },
{ label: "JP_MALE_OSADA", value: "jp_male_osada" },
{ label: "SING_FEMALE_ALTO", value: "en_female_f08_salut_damour" },
{ label: "SING_MALE_TENOR", value: "en_male_m03_lobby" },
{ label: "SING_FEMALE_WARMY_BREEZE", value: "en_female_f08_warmy_breeze" },
{ label: "SING_MALE_SUNSHINE_SOON", value: "en_male_m03_sunshine_soon" },
{ label: "SING_FEMALE_GLORIOUS", value: "en_female_ht_f08_glorious" },
{ label: "SING_MALE_IT_GOES_UP", value: "en_male_sing_funny_it_goes_up" },
{ label: "SING_MALE_CHIPMUNK", value: "en_male_m2_xhxs_m03_silly" },
{ label: "SING_FEMALE_WONDERFUL_WORLD", value: "en_female_ht_f08_wonderful_world" },
{ label: "SING_MALE_FUNNY_THANKSGIVING", value: "en_male_sing_funny_thanksgiving" },
{ label: "MALE_NARRATION", value: "en_male_narration" },
{ label: "MALE_FUNNY", value: "en_male_funny" },
{ label: "FEMALE_EMOTIONAL", value: "en_female_emotional" }
];
return this.voicesLabels;
}

async getMedia(text: string) {
return new Promise<HTMLAudioElement>((resolve, reject) => {
try {
fetch("https://tiktok-tts.weilnet.workers.dev/api/generation", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: text, voice: this.selectedVoice })
}).then(async response => {
const data = await response.json();
const audio = new Audio();
audio.src = `data:audio/mpeg;base64,${data.data}`;
audio.addEventListener("loadeddata", () => resolve(audio));
audio.addEventListener("error", () => reject(new Error("Failed to load audio")));
});
} catch (error) {
reject(new Error("Failed to load audio"));
}
});
}
};
67 changes: 67 additions & 0 deletions src/plugins/betterTTS/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/

import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";

import { annouceUser, messageRecieved, speakMessage, stopTTS } from "./libraries/actions";
import AudioPlayer from "./libraries/AudioPlayer";
import { PatchChannelContextMenu, PatchGuildContextMenu, PatchUserContextMenu } from "./libraries/ContextMenu";
import { onKeyDown, updateRelationships } from "./libraries/utils";
import settings from "./settings";

export default definePlugin({
name: "BetterTTS",
description: "A plugin that allows you to play a custom TTS when a message is received.",
authors: [Devs.nicola02nb],
settings,
contextMenus: {
"user-context": PatchUserContextMenu,
"channel-context": PatchChannelContextMenu,
"guild-context": PatchGuildContextMenu,
"guild-header-popout": PatchGuildContextMenu
},
flux: {
AUDIO_TOGGLE_SELF_DEAF: stopTTS,
VOICE_STATE_UPDATES: annouceUser,
SPEAK_MESSAGE: speakMessage,
MESSAGE_CREATE: messageRecieved,
RELATIONSHIP_ADD: updateRelationships,
RELATIONSHIP_REMOVE: updateRelationships,
},
patches: [
{
find: "new SpeechSynthesisUtterance(",
group: true,
replacement: [
{
match: /\((\i),(\i)\){(\i)&&[\s\S]*?,speechSynthesis\.speak\((\i)\)[\s\S]*?}/,
replace: "($1,$2){return;}"
},
{
match: /\(\){[\s\S]*?speechSynthesis\.cancel\(\)[\s\S]*?}/,
replace: "() {return;}"
}
]
},
{
find: "default.setTTSType",
replacement: {
match: /default.setTTSType\((\i)\)/,
replace: "default.setTTSType('NONE')"
}
}
],
start: () => {
document.addEventListener("keydown", onKeyDown);
AudioPlayer.updateConfig(settings.store.ttsSource, settings.store.ttsVoice, settings.store.ttsSpeechRate, settings.store.ttsDelayBetweenMessages, settings.store.ttsVolume);
},
stop: () => {
AudioPlayer.stopTTS();
document.removeEventListener("keydown", onKeyDown);
}
});

Loading