Blame Raw
· · · 181 lines (6.8 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';
4
5export { OpenAICompatibleTtsProvider };
6
7class OpenAICompatibleTtsProvider {
8 settings;
9 voices = [];
10 separator = ' . ';
11
12 audioElement = document.createElement('audio');
13
14 defaultSettings = {
15 voiceMap: {},
16 model: 'tts-1',
17 speed: 1,
18 available_voices: ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'],
19 provider_endpoint: 'http://127.0.0.1:8000/v1/audio/speech',
20 };
21
22 get settingsHtml() {
23 let html = `
24 <label for="openai_compatible_tts_endpoint">Provider Endpoint:</label>
25 <div class="flex-container alignItemsCenter">
26 <div class="flex1">
27 <input id="openai_compatible_tts_endpoint" type="text" class="text_pole" maxlength="500" value="${this.defaultSettings.provider_endpoint}"/>
28 </div>
29 <div id="openai_compatible_tts_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_custom_openai_tts">
30 <i class="fa-solid fa-key"></i>
31 <span>API Key</span>
32 </div>
33 </div>
34 <label for="openai_compatible_model">Model:</label>
35 <input id="openai_compatible_model" type="text" class="text_pole" maxlength="500" value="${this.defaultSettings.model}"/>
36 <label for="openai_compatible_tts_voices">Available Voices (comma separated):</label>
37 <input id="openai_compatible_tts_voices" type="text" class="text_pole" value="${this.defaultSettings.available_voices.join()}"/>
38 <label for="openai_compatible_tts_speed">Speed: <span id="openai_compatible_tts_speed_output"></span></label>
39 <input type="range" id="openai_compatible_tts_speed" value="1" min="0.25" max="4" step="0.05">`;
40 return html;
41 }
42
43 constructor() {
44 this.handler = async function (/** @type {string} */ key) {
45 if (key !== SECRET_KEYS.CUSTOM_OPENAI_TTS) return;
46 $('#openai_compatible_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.CUSTOM_OPENAI_TTS]);
47 await this.onRefreshClick();
48 }.bind(this);
49 }
50
51 dispose() {
52 [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
53 eventSource.removeListener(event, this.handler);
54 });
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_compatible_tts_endpoint').val(this.settings.provider_endpoint);
75 $('#openai_compatible_tts_endpoint').on('input', () => { this.onSettingsChange(); });
76
77 $('#openai_compatible_model').val(this.defaultSettings.model);
78 $('#openai_compatible_model').on('input', () => { this.onSettingsChange(); });
79
80 $('#openai_compatible_tts_voices').val(this.settings.available_voices.join());
81 $('#openai_compatible_tts_voices').on('input', () => { this.onSettingsChange(); });
82
83 $('#openai_compatible_tts_speed').val(this.settings.speed);
84 $('#openai_compatible_tts_speed').on('input', () => {
85 this.onSettingsChange();
86 });
87
88 $('#openai_compatible_tts_speed_output').text(this.settings.speed);
89
90 $('#openai_compatible_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.CUSTOM_OPENAI_TTS]);
91 [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
92 eventSource.on(event, this.handler);
93 });
94
95 await this.checkReady();
96
97 console.debug('OpenAI Compatible TTS: Settings loaded');
98 }
99
100 onSettingsChange() {
101 // Update dynamically
102 this.settings.provider_endpoint = String($('#openai_compatible_tts_endpoint').val());
103 this.settings.model = String($('#openai_compatible_model').val());
104 this.settings.available_voices = String($('#openai_compatible_tts_voices').val()).split(',');
105 this.settings.speed = Number($('#openai_compatible_tts_speed').val());
106 $('#openai_compatible_tts_speed_output').text(this.settings.speed);
107 saveTtsProviderSettings();
108 }
109
110 async checkReady() {
111 this.voices = await this.fetchTtsVoiceObjects();
112 }
113
114 async onRefreshClick() {
115 return;
116 }
117
118 async getVoice(voiceName) {
119 if (this.voices.length == 0) {
120 this.voices = await this.fetchTtsVoiceObjects();
121 }
122 const match = this.voices.filter(
123 oaicVoice => oaicVoice.name == voiceName,
124 )[0];
125 if (!match) {
126 throw `TTS Voice name ${voiceName} not found`;
127 }
128 return match;
129 }
130
131 async generateTts(text, voiceId) {
132 const response = await this.fetchTtsGeneration(text, voiceId);
133 return response;
134 }
135
136 async fetchTtsVoiceObjects() {
137 return this.settings.available_voices.map(v => {
138 return { name: v, voice_id: v, lang: 'en-US' };
139 });
140 }
141
142 async previewTtsVoice(voiceId) {
143 this.audioElement.pause();
144 this.audioElement.currentTime = 0;
145
146 const text = getPreviewString('en-US');
147 const response = await this.fetchTtsGeneration(text, voiceId);
148 if (!response.ok) {
149 throw new Error(`HTTP ${response.status}`);
150 }
151
152 const audio = await response.blob();
153 const url = URL.createObjectURL(audio);
154 this.audioElement.src = url;
155 this.audioElement.play();
156 this.audioElement.onended = () => URL.revokeObjectURL(url);
157 }
158
159 async fetchTtsGeneration(inputText, voiceId) {
160 console.info(`Generating new TTS for voice_id ${voiceId}`);
161 const response = await fetch('/api/openai/custom/generate-voice', {
162 method: 'POST',
163 headers: getRequestHeaders(),
164 body: JSON.stringify({
165 provider_endpoint: this.settings.provider_endpoint,
166 model: this.settings.model,
167 input: inputText,
168 voice: voiceId,
169 response_format: 'mp3',
170 speed: this.settings.speed,
171 }),
172 });
173
174 if (!response.ok) {
175 toastr.error(response.statusText, 'TTS Generation Failed');
176 throw new Error(`HTTP ${response.status}: ${await response.text()}`);
177 }
178
179 return response;
180 }
181}