Merge pull request #2979 from SillyTavern/google-tts TTS: Add Google Translate TTS

6c9ff8bb3d253de2805e459358f5552b145eee88

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
3 files changed, +166 -0Showing whitespace changes
public/scripts/extensions/tts/google-translate.js+140 -0
@@ -0,0 +1,140 @@
1+import { getRequestHeaders } from '../../../script.js';
2+import { splitRecursive } from '../../utils.js';
3+import { getPreviewString, saveTtsProviderSettings } from './index.js';
4+export { GoogleTranslateTtsProvider };
5+
6+class GoogleTranslateTtsProvider {
7+ settings;
8+ voices = [];
9+ separator = ' . ';
10+ audioElement = document.createElement('audio');
11+
12+ defaultSettings = {
13+ region: '',
14+ voiceMap: {},
15+ };
16+
17+ get settingsHtml() {
18+ return '';
19+ }
20+
21+ onSettingsChange() {
22+ this.voices = [];
23+ saveTtsProviderSettings();
24+ }
25+
26+ async loadSettings(settings) {
27+ // Populate Provider UI given input settings
28+ if (Object.keys(settings).length == 0) {
29+ console.info('Using default TTS Provider settings');
30+ }
31+
32+ // Only accept keys defined in defaultSettings
33+ this.settings = this.defaultSettings;
34+
35+ for (const key in settings) {
36+ if (key in this.settings) {
37+ this.settings[key] = settings[key];
38+ } else {
39+ throw `Invalid setting passed to TTS Provider: ${key}`;
40+ }
41+ }
42+
43+ try {
44+ await this.checkReady();
45+ console.debug('Google Translate TTS: Settings loaded');
46+ } catch {
47+ console.debug('Google Translate TTS: Settings loaded, but not ready');
48+ }
49+ }
50+
51+ // Perform a simple readiness check by trying to fetch voiceIds
52+ async checkReady() {
53+ await this.fetchTtsVoiceObjects();
54+ }
55+
56+ async onRefreshClick() {
57+ await this.checkReady();
58+ }
59+
60+ //#################//
61+ // TTS Interfaces //
62+ //#################//
63+
64+ async getVoice(voiceName) {
65+ if (this.voices.length == 0) {
66+ this.voices = await this.fetchTtsVoiceObjects();
67+ }
68+ const match = this.voices.filter(
69+ voice => voice.name == voiceName || voice.voice_id == voiceName,
70+ )[0];
71+ if (!match) {
72+ throw `TTS Voice name ${voiceName} not found`;
73+ }
74+ return match;
75+ }
76+
77+ async generateTts(text, voiceId) {
78+ const response = await this.fetchTtsGeneration(text, voiceId);
79+ return response;
80+ }
81+
82+ //###########//
83+ // API CALLS //
84+ //###########//
85+ async fetchTtsVoiceObjects() {
86+ const response = await fetch('/api/google/list-voices', {
87+ method: 'POST',
88+ headers: getRequestHeaders(),
89+ body: JSON.stringify({}),
90+ });
91+
92+ if (!response.ok) {
93+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
94+ }
95+ let responseJson = await response.json();
96+ responseJson = Object.entries(responseJson)
97+ .sort((a, b) => a[1].localeCompare(b[1]))
98+ .map(x => ({ name: x[1], voice_id: x[0], preview_url: false, lang: x[0] }));
99+ return responseJson;
100+ }
101+
102+ /**
103+ * Preview TTS for a given voice ID.
104+ * @param {string} id Voice ID
105+ */
106+ async previewTtsVoice(id) {
107+ this.audioElement.pause();
108+ this.audioElement.currentTime = 0;
109+ const voice = await this.getVoice(id);
110+ const text = getPreviewString(voice.lang);
111+ const response = await this.fetchTtsGeneration(text, id);
112+ if (!response.ok) {
113+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
114+ }
115+
116+ const audio = await response.blob();
117+ const url = URL.createObjectURL(audio);
118+ this.audioElement.src = url;
119+ this.audioElement.play();
120+ this.audioElement.onended = () => URL.revokeObjectURL(url);
121+ }
122+
123+ async fetchTtsGeneration(text, voiceId) {
124+ const response = await fetch('/api/google/generate-voice', {
125+ method: 'POST',
126+ headers: getRequestHeaders(),
127+ body: JSON.stringify({
128+ text: splitRecursive(text, 200),
129+ voice: voiceId,
130+ }),
131+ });
132+
133+ if (!response.ok) {
134+ toastr.error(response.statusText, 'TTS Generation Failed');
135+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
136+ }
137+
138+ return response;
139+ }
140+}
public/scripts/extensions/tts/index.js+2 -0
@@ -26,6 +26,7 @@ import { debounce_timeout } from '../../constants.js';
2626import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
2727import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
2828import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
29+import { GoogleTranslateTtsProvider } from './google-translate.js';
2930export { talkingAnimation };
3031
3132const UPDATE_INTERVAL = 1000;
@@ -91,6 +92,7 @@ const ttsProviders = {
9192 'CosyVoice (Unofficial)': CosyVoiceProvider,
9293 Edge: EdgeTtsProvider,
9394 ElevenLabs: ElevenLabsTtsProvider,
95+ 'Google Translate': GoogleTranslateTtsProvider,
9496 GSVI: GSVITtsProvider,
9597 'GPT-SoVITS-V2 (Unofficial)': GptSovitsV2Provider,
9698 Novel: NovelTtsProvider,
src/endpoints/google.js+24 -0
@@ -1,5 +1,7 @@
1+import { Buffer } from 'node:buffer';
12import fetch from 'node-fetch';
23import express from 'express';
4+import { speak, languages } from 'google-translate-api-x';
35
46import { readSecret, SECRET_KEYS } from './secrets.js';
57import { jsonParser } from '../express-common.js';
@@ -68,3 +70,25 @@ router.post('/caption-image', jsonParser, async (request, response) => {
6870 response.status(500).send('Internal server error');
6971 }
7072});
73+
74+router.post('/list-voices', (_, response) => {
75+ return response.json(languages);
76+});
77+
78+router.post('/generate-voice', jsonParser, async (request, response) => {
79+ try {
80+ const text = request.body.text;
81+ const voice = request.body.voice ?? 'en';
82+
83+ const result = await speak(text, { to: voice, forceBatch: false });
84+ const buffer = Array.isArray(result)
85+ ? Buffer.concat(result.map(x => new Uint8Array(Buffer.from(x.toString(), 'base64'))))
86+ : Buffer.from(result.toString(), 'base64');
87+
88+ response.setHeader('Content-Type', 'audio/mpeg');
89+ return response.send(buffer);
90+ } catch (error) {
91+ console.error('Google Translate TTS generation failed', error);
92+ response.status(500).send('Internal server error');
93+ }
94+});