Blame Raw
· · · 208 lines (6.5 KB)
0 contributors
1import { event_types, eventSource, getRequestHeaders } from '../../../script.js';
2import { SECRET_KEYS, secret_state } from '../../secrets.js';
3import { getPreviewString, saveTtsProviderSettings } from './index.js';
4export { AzureTtsProvider };
5
6class AzureTtsProvider {
7 //########//
8 // Config //
9 //########//
10
11 settings;
12 voices = [];
13 separator = ' . ';
14 audioElement = document.createElement('audio');
15
16 defaultSettings = {
17 region: '',
18 voiceMap: {},
19 };
20
21 get settingsHtml() {
22 let html = `
23 <div class="azure_tts_settings">
24 <div class="flex-container alignItemsBaseline">
25 <h4 for="azure_tts_key" class="flex1 margin0">
26 <a href="https://portal.azure.com/" target="_blank">Azure TTS Key</a>
27 </h4>
28 <div id="azure_tts_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_azure_tts">
29 <i class="fa-solid fa-key"></i>
30 <span>Click to set</span>
31 </div>
32 </div>
33 <label for="azure_tts_region">Region:</label>
34 <input id="azure_tts_region" type="text" class="text_pole" placeholder="e.g. westus" />
35 <hr>
36 </div>
37 `;
38 return html;
39 }
40
41 constructor() {
42 this.handler = async function (/** @type {string} */ key) {
43 if (key !== SECRET_KEYS.AZURE_TTS) return;
44 $('#azure_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.AZURE_TTS]);
45 await this.onRefreshClick();
46 }.bind(this);
47 }
48
49 dispose() {
50 [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
51 eventSource.removeListener(event, this.handler);
52 });
53 }
54
55 onSettingsChange() {
56 // Update dynamically
57 this.settings.region = String($('#azure_tts_region').val());
58 // Reset voices
59 this.voices = [];
60 saveTtsProviderSettings();
61 }
62
63 async loadSettings(settings) {
64 // Populate Provider UI given input settings
65 if (Object.keys(settings).length == 0) {
66 console.info('Using default TTS Provider settings');
67 }
68
69 // Only accept keys defined in defaultSettings
70 this.settings = this.defaultSettings;
71
72 for (const key in settings) {
73 if (key in this.settings) {
74 this.settings[key] = settings[key];
75 } else {
76 throw `Invalid setting passed to TTS Provider: ${key}`;
77 }
78 }
79
80 $('#azure_tts_region').val(this.settings.region).on('input', () => this.onSettingsChange());
81 $('#azure_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.AZURE_TTS]);
82 [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
83 eventSource.on(event, this.handler);
84 });
85
86 try {
87 await this.checkReady();
88 console.debug('Azure: Settings loaded');
89 } catch {
90 console.debug('Azure: Settings loaded, but not ready');
91 }
92 }
93
94 // Perform a simple readiness check by trying to fetch voiceIds
95 async checkReady() {
96 if (secret_state[SECRET_KEYS.AZURE_TTS]) {
97 await this.fetchTtsVoiceObjects();
98 } else {
99 this.voices = [];
100 }
101 }
102
103 async onRefreshClick() {
104 await this.checkReady();
105 }
106
107 //#################//
108 // TTS Interfaces //
109 //#################//
110
111 async getVoice(voiceName) {
112 if (this.voices.length == 0) {
113 this.voices = await this.fetchTtsVoiceObjects();
114 }
115 const match = this.voices.filter(
116 voice => voice.name == voiceName,
117 )[0];
118 if (!match) {
119 throw `TTS Voice name ${voiceName} not found`;
120 }
121 return match;
122 }
123
124 async generateTts(text, voiceId) {
125 const response = await this.fetchTtsGeneration(text, voiceId);
126 return response;
127 }
128
129 //###########//
130 // API CALLS //
131 //###########//
132 async fetchTtsVoiceObjects() {
133 if (!secret_state[SECRET_KEYS.AZURE_TTS]) {
134 console.warn('Azure TTS API Key not set');
135 return [];
136 }
137
138 if (!this.settings.region) {
139 console.warn('Azure TTS region not set');
140 return [];
141 }
142
143 const response = await fetch('/api/azure/list', {
144 method: 'POST',
145 headers: getRequestHeaders(),
146 body: JSON.stringify({
147 region: this.settings.region,
148 }),
149 });
150
151 if (!response.ok) {
152 throw new Error(`HTTP ${response.status}: ${await response.text()}`);
153 }
154 let responseJson = await response.json();
155 responseJson = responseJson
156 .sort((a, b) => a.Locale.localeCompare(b.Locale) || a.ShortName.localeCompare(b.ShortName))
157 .map(x => ({ name: x.ShortName, voice_id: x.ShortName, preview_url: false, lang: x.Locale }));
158 return responseJson;
159 }
160
161 /**
162 * Preview TTS for a given voice ID.
163 * @param {string} id Voice ID
164 */
165 async previewTtsVoice(id) {
166 this.audioElement.pause();
167 this.audioElement.currentTime = 0;
168 const voice = await this.getVoice(id);
169 const text = getPreviewString(voice.lang);
170 const response = await this.fetchTtsGeneration(text, id);
171 if (!response.ok) {
172 throw new Error(`HTTP ${response.status}: ${await response.text()}`);
173 }
174
175 const audio = await response.blob();
176 const url = URL.createObjectURL(audio);
177 this.audioElement.src = url;
178 this.audioElement.play();
179 this.audioElement.onended = () => URL.revokeObjectURL(url);
180 }
181
182 async fetchTtsGeneration(text, voiceId) {
183 if (!secret_state[SECRET_KEYS.AZURE_TTS]) {
184 throw new Error('Azure TTS API Key not set');
185 }
186
187 if (!this.settings.region) {
188 throw new Error('Azure TTS region not set');
189 }
190
191 const response = await fetch('/api/azure/generate', {
192 method: 'POST',
193 headers: getRequestHeaders(),
194 body: JSON.stringify({
195 text: text,
196 voice: voiceId,
197 region: this.settings.region,
198 }),
199 });
200
201 if (!response.ok) {
202 toastr.error(response.statusText, 'TTS Generation Failed');
203 throw new Error(`HTTP ${response.status}: ${await response.text()}`);
204 }
205
206 return response;
207 }
208}