Blame Raw
Cohee · e3f41666 · · 252 lines (9.2 KB)
1 contributor
1import { getRequestHeaders, substituteParams } from '../../../script.js';
2import { saveTtsProviderSettings, sanitizeId } from './index.js';
3
4export { OpenAITtsProvider };
5
6class OpenAITtsProvider {
7 static voices = [
8 { name: 'Alloy', voice_id: 'alloy', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/alloy.wav' },
9 { name: 'Ash', voice_id: 'ash', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/ash.wav' },
10 { name: 'Coral', voice_id: 'coral', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/coral.wav' },
11 { name: 'Echo', voice_id: 'echo', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/echo.wav' },
12 { name: 'Fable', voice_id: 'fable', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/fable.wav' },
13 { name: 'Onyx', voice_id: 'onyx', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/onyx.wav' },
14 { name: 'Nova', voice_id: 'nova', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/nova.wav' },
15 { name: 'Sage', voice_id: 'sage', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/sage.wav' },
16 { name: 'Shimmer', voice_id: 'shimmer', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/shimmer.wav' },
17 ];
18
19 settings;
20 voices = [];
21 separator = ' . ';
22 audioElement = document.createElement('audio');
23
24 defaultSettings = {
25 voiceMap: {},
26 customVoices: [],
27 model: 'tts-1',
28 speed: 1,
29 characterInstructions: {},
30 };
31
32 get settingsHtml() {
33 let html = `
34 <div>Use OpenAI's TTS engine.</div>
35 <small>Hint: Save an API key in the OpenAI API settings to use it here.</small>
36 <div>
37 <label for="openai-tts-model">Model:</label>
38 <select id="openai-tts-model">
39 <optgroup label="Latest">
40 <option value="tts-1">tts-1</option>
41 <option value="tts-1-hd">tts-1-hd</option>
42 <option value="gpt-4o-mini-tts">gpt-4o-mini-tts</option>
43 </optgroup>
44 <optgroup label="Snapshots">
45 <option value="tts-1-1106">tts-1-1106</option>
46 <option value="tts-1-hd-1106">tts-1-hd-1106</option>
47 </optgroup>
48 <select>
49 </div>
50 <div>
51 <label for="openai-tts-speed">Speed: <span id="openai-tts-speed-output"></span></label>
52 <input type="range" id="openai-tts-speed" value="1" min="0.25" max="4" step="0.05">
53 </div>`;
54 return html;
55 }
56
57 async loadSettings(settings) {
58 // Populate Provider UI given input settings
59 if (Object.keys(settings).length == 0) {
60 console.info('Using default TTS Provider settings');
61 }
62
63 // Only accept keys defined in defaultSettings
64 this.settings = this.defaultSettings;
65
66 for (const key in settings) {
67 if (key in this.settings) {
68 this.settings[key] = settings[key];
69 } else {
70 throw `Invalid setting passed to TTS Provider: ${key}`;
71 }
72 }
73
74 $('#openai-tts-model').val(this.settings.model);
75 $('#openai-tts-model').on('change', () => {
76 this.onSettingsChange();
77 });
78
79 $('#openai-tts-speed').val(this.settings.speed);
80 $('#openai-tts-speed').on('input', () => {
81 this.onSettingsChange();
82 });
83
84 $('#openai-tts-speed-output').text(this.settings.speed);
85
86 await this.checkReady();
87 // Initialize UI state based on current model (gpt-4o-mini-tts or other)
88 this.updateInstructionsUI();
89 // Look for voice map changes
90 this.setupVoiceMapObserver();
91
92 console.debug('OpenAI TTS: Settings loaded');
93 }
94
95 setupVoiceMapObserver() {
96 if (this.voiceMapObserver) {
97 this.voiceMapObserver.disconnect();
98 this.voiceMapObserver = null;
99 }
100
101 const targetNode = document.getElementById('tts_voicemap_block');
102 if (!targetNode) return;
103
104 const observer = new MutationObserver(() => {
105 if (this.settings.model === 'gpt-4o-mini-tts') {
106 this.populateCharacterInstructions();
107 }
108 });
109
110 observer.observe(targetNode, { childList: true, subtree: true });
111 this.voiceMapObserver = observer;
112 }
113
114 onSettingsChange() {
115 // Update dynamically
116 this.settings.model = String($('#openai-tts-model').find(':selected').val());
117 this.settings.speed = Number($('#openai-tts-speed').val());
118 $('#openai-tts-speed-output').text(this.settings.speed);
119 this.updateInstructionsUI();
120 saveTtsProviderSettings();
121 }
122
123 updateInstructionsUI() {
124 if (this.settings.model === 'gpt-4o-mini-tts') {
125 this.createInstructionsContainer();
126 $('#openai-instructions-container').show();
127 this.populateCharacterInstructions();
128 } else {
129 $('#openai-instructions-container').hide();
130 this.voiceMapObserver?.disconnect();
131 this.voiceMapObserver = null;
132 }
133 }
134
135 createInstructionsContainer() {
136 if ($('#openai-instructions-container').length === 0) {
137 const containerHtml = `
138 <div id="openai-instructions-container" style="display: none;">
139 <span>Voice Instructions (GPT-4o Mini TTS)</span><br>
140 <small>Customize how each character speaks</small>
141 <div id="openai-character-instructions"></div>
142 </div>
143 `;
144 $('#openai-tts-speed').parent().after(containerHtml);
145 }
146 }
147
148 populateCharacterInstructions() {
149 const currentCharacters = $('.tts_voicemap_block_char span').map((i, el) => $(el).text()).get();
150
151 $('#openai-character-instructions').empty();
152
153 for (const char of currentCharacters) {
154 if (char === 'SillyTavern System' || char === '[Default Voice]') continue;
155
156 const sanitizedName = sanitizeId(char);
157 const savedInstructions = this.settings.characterInstructions?.[char] || '';
158
159 const instructionBlock = document.createElement('div');
160 const label = document.createElement('label');
161 const textArea = document.createElement('textarea');
162 instructionBlock.appendChild(label);
163 instructionBlock.appendChild(textArea);
164 instructionBlock.className = 'character-instructions';
165 label.setAttribute('for', `openai_char_${sanitizedName}`);
166 label.innerText = `${char}:`;
167 textArea.id = `openai_char_${sanitizedName}`;
168 textArea.placeholder = 'e.g., "Speak cheerfully and energetically"';
169 textArea.className = 'textarea_compact autoSetHeight';
170 textArea.value = savedInstructions;
171 textArea.addEventListener('input', () => {
172 this.saveCharacterInstructions(char, textArea.value);
173 });
174
175 $('#openai-character-instructions').append(instructionBlock);
176 }
177 }
178
179 saveCharacterInstructions(characterName, instructions) {
180 if (!this.settings.characterInstructions) {
181 this.settings.characterInstructions = {};
182 }
183 this.settings.characterInstructions[characterName] = instructions;
184 saveTtsProviderSettings();
185 }
186
187 async checkReady() {
188 await this.fetchTtsVoiceObjects();
189 }
190
191 async onRefreshClick() {
192 return;
193 }
194
195 async getVoice(voiceName) {
196 if (!voiceName) {
197 throw 'TTS Voice name not provided';
198 }
199
200 const voice = OpenAITtsProvider.voices.find(voice => voice.voice_id === voiceName || voice.name === voiceName);
201
202 if (!voice) {
203 throw `TTS Voice not found: ${voiceName}`;
204 }
205
206 return voice;
207 }
208
209 async generateTts(text, voiceId, characterName = null) {
210 const response = await this.fetchTtsGeneration(text, voiceId, characterName);
211 return response;
212 }
213
214 async fetchTtsVoiceObjects() {
215 return OpenAITtsProvider.voices;
216 }
217
218 async previewTtsVoice(_) {
219 return;
220 }
221
222 async fetchTtsGeneration(inputText, voiceId, characterName = null) {
223 console.info(`Generating new TTS for voice_id ${voiceId}`);
224
225 const requestBody = {
226 'text': inputText,
227 'voice': voiceId,
228 'model': this.settings.model,
229 'speed': this.settings.speed,
230 };
231
232 if (this.settings.model === 'gpt-4o-mini-tts' && characterName) {
233 const instructions = this.settings.characterInstructions?.[characterName];
234 if (instructions && instructions.trim()) {
235 requestBody.instructions = substituteParams(instructions);
236 }
237 }
238
239 const response = await fetch('/api/openai/generate-voice', {
240 method: 'POST',
241 headers: getRequestHeaders(),
242 body: JSON.stringify(requestBody),
243 });
244
245 if (!response.ok) {
246 toastr.error(response.statusText, 'TTS Generation Failed');
247 throw new Error(`HTTP ${response.status}: ${await response.text()}`);
248 }
249
250 return response;
251 }
252}