TTS: Add Google Translate TTS

8cb0fda321a70adfb4a7f62d65ef470b192f723b

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

3 files changed, +166 -0Showing whitespace changes
public/scripts/extensions/tts/google-translate.js+140 -0
@@ -0,0 +1,140 @@
1import { getRequestHeaders } from '../../../script.js';
2import { splitRecursive } from '../../utils.js';
3import { getPreviewString, saveTtsProviderSettings } from './index.js';
4export { GoogleTranslateTtsProvider };
5
6class 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';
26import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';26import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
27import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';27import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
28import { POPUP_TYPE, callGenericPopup } from '../../popup.js';28import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
29import { GoogleTranslateTtsProvider } from './google-translate.js';
29export { talkingAnimation };30export { talkingAnimation };
3031
31const UPDATE_INTERVAL = 1000;32const UPDATE_INTERVAL = 1000;
@@ -91,6 +92,7 @@ const ttsProviders = {
91 'CosyVoice (Unofficial)': CosyVoiceProvider,92 'CosyVoice (Unofficial)': CosyVoiceProvider,
92 Edge: EdgeTtsProvider,93 Edge: EdgeTtsProvider,
93 ElevenLabs: ElevenLabsTtsProvider,94 ElevenLabs: ElevenLabsTtsProvider,
95 'Google Translate': GoogleTranslateTtsProvider,
94 GSVI: GSVITtsProvider,96 GSVI: GSVITtsProvider,
95 'GPT-SoVITS-V2 (Unofficial)': GptSovitsV2Provider,97 'GPT-SoVITS-V2 (Unofficial)': GptSovitsV2Provider,
96 Novel: NovelTtsProvider,98 Novel: NovelTtsProvider,
src/endpoints/google.js+24 -0
@@ -1,5 +1,7 @@
1import { Buffer } from 'node:buffer';
1import fetch from 'node-fetch';2import fetch from 'node-fetch';
2import express from 'express';3import express from 'express';
4import { speak, languages } from 'google-translate-api-x';
35
4import { readSecret, SECRET_KEYS } from './secrets.js';6import { readSecret, SECRET_KEYS } from './secrets.js';
5import { jsonParser } from '../express-common.js';7import { jsonParser } from '../express-common.js';
@@ -68,3 +70,25 @@ router.post('/caption-image', jsonParser, async (request, response) => {
68 response.status(500).send('Internal server error');70 response.status(500).send('Internal server error');
69 }71 }
70});72});
73
74router.post('/list-voices', (_, response) => {
75 return response.json(languages);
76});
77
78router.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});