Blame Raw
Cohee · e3f41666 · · 193 lines (6.7 KB)
1 contributor
1import { getRequestHeaders } from '../../../script.js';
2import { oai_settings } from '../../openai.js';
3import { isValidUrl } from '../../utils.js';
4import { getPreviewString, saveTtsProviderSettings } from './index.js';
5
6
7export class GoogleNativeTtsProvider {
8 settings;
9 voices = [];
10 separator = ' . ';
11 audioElement = document.createElement('audio');
12
13 defaultSettings = {
14 voiceMap: {},
15 model: 'gemini-2.5-flash-preview-tts',
16 apiType: 'makersuite',
17 };
18
19 get settingsHtml() {
20 return `
21 <small>Hint: Save an API key in the Google AI Studio/Vertex AI connection settings</small>
22 <div id="google-native-tts-settings">
23 <div>
24 <label for="google-tts-api-type">API Type:</label>
25 <select id="google-tts-api-type">
26 <option value="makersuite">Google AI Studio (MakerSuite)</option>
27 <option value="vertexai" disabled>Google Vertex AI (unsupported)</option>
28 </select>
29 </div>
30 <div>
31 <label for="google-tts-model">Model:</label>
32 <select id="google-tts-model">
33 <option value="gemini-2.5-flash-preview-tts">Gemini 2.5 Flash Preview TTS</option>
34 <option value="gemini-2.5-pro-preview-tts">Gemini 2.5 Pro Preview TTS</option>
35 </select>
36 </div>
37 </div>`;
38 }
39
40 async loadSettings(settings) {
41 if (Object.keys(settings).length === 0) {
42 console.info('Using default Google TTS Provider settings');
43 }
44
45 this.settings = { ...this.defaultSettings, ...settings };
46
47 $('#google-tts-api-type').val(this.settings.apiType);
48 $('#google-tts-model').val(this.settings.model);
49
50 $('#google-tts-api-type, #google-tts-model').on('change', () => this.onSettingsChange());
51
52 try {
53 await this.checkReady();
54 console.debug('Google TTS: Settings loaded');
55 } catch (err) {
56 console.warn('Google TTS: Settings loaded, but not ready.', err.message);
57 }
58 }
59
60 onSettingsChange() {
61 this.settings.apiType = $('#google-tts-api-type').val();
62 this.settings.model = $('#google-tts-model').val();
63
64 this.voices = []; // Reset voices cache so it re-fetches
65 saveTtsProviderSettings();
66 }
67
68 async checkReady() {
69 await this.fetchTtsVoiceObjects();
70 }
71
72 async onRefreshClick() {
73 await this.checkReady();
74 }
75
76 async getVoice(voiceName) {
77 if (this.voices.length === 0) {
78 this.voices = await this.fetchTtsVoiceObjects();
79 }
80
81 const match = this.voices.find(voice => voice.name === voiceName || voice.voice_id === voiceName);
82
83 if (!match) {
84 throw `TTS Voice name ${voiceName} not found`;
85 }
86 return match;
87 }
88
89 async generateTts(text, voiceId) {
90 return await this.fetchNativeTtsGeneration(text, voiceId);
91 }
92
93 async fetchTtsVoiceObjects() {
94 try {
95 const response = await fetch('/api/google/list-native-voices', {
96 method: 'POST',
97 headers: getRequestHeaders(),
98 body: JSON.stringify({}),
99 });
100
101 if (!response.ok) {
102 let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
103
104 try {
105 const errorJson = await response.json();
106 if (errorJson.error) {
107 errorMessage = errorJson.error;
108 }
109 } catch (parseError) {
110 // Response isn't valid JSON, use the HTTP error message
111 console.debug('Error response is not JSON:', parseError.message);
112 }
113
114 throw new Error(errorMessage);
115 }
116
117 const responseJson = await response.json();
118
119 if (!responseJson.voices || !Array.isArray(responseJson.voices)) {
120 throw new Error('Invalid response format: voices array not found');
121 }
122
123 this.voices = responseJson.voices;
124 console.info(`Google TTS: Loaded ${this.voices.length} voices`);
125
126 return this.voices;
127 } catch (error) {
128 console.error('Failed to fetch Google TTS voices:', error);
129 throw error;
130 }
131 }
132
133 async previewTtsVoice(id) {
134 this.audioElement.pause();
135 this.audioElement.currentTime = 0;
136
137 try {
138 const voice = await this.getVoice(id);
139 const text = getPreviewString(voice.lang || 'en-US');
140
141 const response = await this.fetchNativeTtsGeneration(text, id);
142
143 if (!response.ok) {
144 // Error is handled inside the fetch function, but we still need to stop here
145 return;
146 }
147
148 const audioBlob = await response.blob();
149 const url = URL.createObjectURL(audioBlob);
150 this.audioElement.src = url;
151 this.audioElement.play();
152 this.audioElement.onended = () => URL.revokeObjectURL(url);
153 } catch (error) {
154 console.error('TTS Preview Error:', error);
155 toastr.error(`Could not generate preview: ${error.message}`);
156 }
157 }
158
159 async fetchNativeTtsGeneration(text, voiceId) {
160 console.info(`Generating native Google TTS for voice_id ${voiceId}`);
161 const useReverseProxy = oai_settings.reverse_proxy && isValidUrl(oai_settings.reverse_proxy);
162
163 const response = await fetch('/api/google/generate-native-tts', {
164 method: 'POST',
165 headers: getRequestHeaders(),
166 body: JSON.stringify({
167 text: text,
168 voice: voiceId,
169 model: this.settings.model,
170 api: this.settings.apiType,
171 reverse_proxy: useReverseProxy ? oai_settings.reverse_proxy : '',
172 proxy_password: useReverseProxy ? oai_settings.proxy_password : '',
173 vertexai_auth_mode: oai_settings.vertexai_auth_mode,
174 vertexai_region: oai_settings.vertexai_region,
175 vertexai_express_project_id: oai_settings.vertexai_express_project_id,
176 }),
177 });
178
179 if (!response.ok) {
180 let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
181 try {
182 const errorJson = await response.json();
183 if (errorJson.error) {
184 errorMessage = errorJson.error;
185 }
186 } catch {
187 // Not a JSON response, do nothing and keep the original http error
188 }
189 throw new Error(errorMessage);
190 }
191 return response;
192 }
193}