Blame Raw
· · · 233 lines (9.4 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 { ChutesTtsProvider };
6
7class ChutesTtsProvider {
8 settings;
9 voices = [];
10 models = [];
11 separator = ' . ';
12
13 defaultSettings = {
14 voiceMap: {},
15 model: 'kokoro',
16 speed: 1,
17 };
18
19 get settingsHtml() {
20 let html = `
21 <div class="flex-container alignItemsCenter">
22 <div class="flex1">Chutes TTS API</div>
23 <div id="chutes_tts_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_chutes">
24 <i class="fa-solid fa-key"></i>
25 <span>API Key</span>
26 </div>
27 </div>
28 <div class="flex-container flexFlowColumn">
29 <div class="flex1">
30 <label for="chutes_tts_model">Model</label>
31 <select id="chutes_tts_model" class="text_pole"></select>
32 </div>
33 <div>
34 <label for="chutes_tts_speed">Speed <span id="chutes_tts_speed_output"></span></label>
35 <input type="range" id="chutes_tts_speed" value="1" min="0.25" max="3" step="0.05">
36 </div>
37 </div>`;
38 return html;
39 }
40
41 constructor() {
42 this.handler = async function (/** @type {string} */ key) {
43 if (key !== SECRET_KEYS.CHUTES) return;
44 $('#chutes_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.CHUTES]);
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 this.settings.model = $('#chutes_tts_model').val();
57 this.settings.speed = Number($('#chutes_tts_speed').val());
58 saveTtsProviderSettings();
59 }
60
61 async loadSettings(settings) {
62 if (Object.keys(settings).length === 0) {
63 Object.assign(settings, this.defaultSettings);
64 }
65
66 this.settings = settings;
67
68 if (!this.settings.voiceMap) {
69 this.settings.voiceMap = {};
70 }
71
72 // Update UI
73 $('#chutes_tts_model').val(this.settings.model);
74 $('#chutes_tts_speed').val(this.settings.speed);
75 $('#chutes_tts_speed_output').text(this.settings.speed);
76
77 $('#chutes_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.CHUTES]);
78 [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
79 eventSource.on(event, this.handler);
80 });
81
82 await this.checkReady();
83
84 $('#chutes_tts_model').on('change', () => this.onSettingsChange());
85 $('#chutes_tts_speed').on('input', () => {
86 const value = $('#chutes_tts_speed').val();
87 $('#chutes_tts_speed_output').text(String(value));
88 this.onSettingsChange();
89 });
90 }
91
92 async checkReady() {
93 await this.updateModels();
94 if (this.models.length === 0) {
95 // No models available
96 }
97 await this.updateVoices();
98 }
99
100 async onRefreshClick() {
101 return await this.checkReady();
102 }
103
104 async updateModels() {
105 // For Chutes TTS, we always use the Kokoro model currently.
106 this.models = ['kokoro'];
107
108 $('#chutes_tts_model').empty();
109 $('#chutes_tts_model').append($('<option>').val('kokoro').text('Kokoro'));
110 $('#chutes_tts_model').val('kokoro');
111
112 this.settings.model = 'kokoro';
113 }
114
115 async updateVoices() {
116 // Kokoro voices list
117 const kokoroVoices = [
118 { id: 'af_alloy', name: 'Alloy (Female)', lang: 'en-US' },
119 { id: 'af_aoede', name: 'Aoede (Female)', lang: 'en-US' },
120 { id: 'af_bella', name: 'Bella (Female)', lang: 'en-US' },
121 { id: 'af_heart', name: 'Heart (Female) - Default', lang: 'en-US' },
122 { id: 'af_jessica', name: 'Jessica (Female)', lang: 'en-US' },
123 { id: 'af_kore', name: 'Kore (Female)', lang: 'en-US' },
124 { id: 'af_nicole', name: 'Nicole (Female)', lang: 'en-US' },
125 { id: 'af_nova', name: 'Nova (Female)', lang: 'en-US' },
126 { id: 'af_river', name: 'River (Female)', lang: 'en-US' },
127 { id: 'af_sarah', name: 'Sarah (Female)', lang: 'en-US' },
128 { id: 'af_sky', name: 'Sky (Female)', lang: 'en-US' },
129 { id: 'am_adam', name: 'Adam (Male)', lang: 'en-US' },
130 { id: 'am_echo', name: 'Echo (Male)', lang: 'en-US' },
131 { id: 'am_eric', name: 'Eric (Male)', lang: 'en-US' },
132 { id: 'am_fenrir', name: 'Fenrir (Male)', lang: 'en-US' },
133 { id: 'am_liam', name: 'Liam (Male)', lang: 'en-US' },
134 { id: 'am_michael', name: 'Michael (Male)', lang: 'en-US' },
135 { id: 'am_onyx', name: 'Onyx (Male)', lang: 'en-US' },
136 { id: 'am_puck', name: 'Puck (Male)', lang: 'en-US' },
137 { id: 'am_santa', name: 'Santa (Male)', lang: 'en-US' },
138 { id: 'bf_alice', name: 'Alice (British Female)', lang: 'en-GB' },
139 { id: 'bf_emma', name: 'Emma (British Female)', lang: 'en-GB' },
140 { id: 'bf_isabella', name: 'Isabella (British Female)', lang: 'en-GB' },
141 { id: 'bf_lily', name: 'Lily (British Female)', lang: 'en-GB' },
142 { id: 'bm_daniel', name: 'Daniel (British Male)', lang: 'en-GB' },
143 { id: 'bm_fable', name: 'Fable (British Male)', lang: 'en-GB' },
144 { id: 'bm_george', name: 'George (British Male)', lang: 'en-GB' },
145 { id: 'bm_lewis', name: 'Lewis (British Male)', lang: 'en-GB' },
146 { id: 'ef_dora', name: 'Dora (European Female)', lang: 'es-ES' },
147 { id: 'em_alex', name: 'Alex (European Male)', lang: 'es-ES' },
148 { id: 'em_santa', name: 'Santa (European Male)', lang: 'es-ES' },
149 { id: 'ff_siwis', name: 'Siwis (French Female)', lang: 'fr-FR' },
150 { id: 'hf_alpha', name: 'Alpha (Hindi Female)', lang: 'hi-IN' },
151 { id: 'hf_beta', name: 'Beta (Hindi Female)', lang: 'hi-IN' },
152 { id: 'hm_omega', name: 'Omega (Hindi Male)', lang: 'hi-IN' },
153 { id: 'hm_psi', name: 'Psi (Hindi Male)', lang: 'hi-IN' },
154 { id: 'if_sara', name: 'Sara (Italian Female)', lang: 'it-IT' },
155 { id: 'im_nicola', name: 'Nicola (Italian Male)', lang: 'it-IT' },
156 { id: 'jf_alpha', name: 'Alpha (Japanese Female)', lang: 'ja-JP' },
157 { id: 'jf_gongitsune', name: 'Gongitsune (Japanese Female)', lang: 'ja-JP' },
158 { id: 'jf_nezumi', name: 'Nezumi (Japanese Female)', lang: 'ja-JP' },
159 { id: 'jf_tebukuro', name: 'Tebukuro (Japanese Female)', lang: 'ja-JP' },
160 { id: 'jm_kumo', name: 'Kumo (Japanese Male)', lang: 'ja-JP' },
161 { id: 'pf_dora', name: 'Dora (Portuguese Female)', lang: 'pt-PT' },
162 { id: 'pm_alex', name: 'Alex (Portuguese Male)', lang: 'pt-PT' },
163 { id: 'pm_santa', name: 'Santa (Portuguese Male)', lang: 'pt-PT' },
164 { id: 'zf_xiaobei', name: 'Xiaobei (Chinese Female)', lang: 'zh-CN' },
165 { id: 'zf_xiaoni', name: 'Xiaoni (Chinese Female)', lang: 'zh-CN' },
166 { id: 'zf_xiaoxiao', name: 'Xiaoxiao (Chinese Female)', lang: 'zh-CN' },
167 { id: 'zf_xiaoyi', name: 'Xiaoyi (Chinese Female)', lang: 'zh-CN' },
168 { id: 'zm_yunjian', name: 'Yunjian (Chinese Male)', lang: 'zh-CN' },
169 { id: 'zm_yunxi', name: 'Yunxi (Chinese Male)', lang: 'zh-CN' },
170 { id: 'zm_yunxia', name: 'Yunxia (Chinese Male)', lang: 'zh-CN' },
171 { id: 'zm_yunyang', name: 'Yunyang (Chinese Male)', lang: 'zh-CN' },
172 ];
173
174 this.voices = kokoroVoices.map(v => ({
175 name: v.name,
176 voice_id: v.id,
177 lang: v.lang,
178 }));
179 }
180
181 async getVoice(voiceName) {
182 if (this.voices.length === 0) {
183 await this.updateVoices();
184 }
185 const voice = this.voices.find(v => v.name === voiceName || v.voice_id === voiceName);
186 return voice || this.voices.find(v => v.voice_id === 'af_heart');
187 }
188
189 async generateTts(text, voiceId) {
190 const response = await this.fetchTtsGeneration(text, voiceId);
191 return response;
192 }
193
194 async fetchTtsGeneration(text, voiceId) {
195 const apiKey = secret_state[SECRET_KEYS.CHUTES];
196
197 if (!apiKey) {
198 throw new Error('No Chutes API key found');
199 }
200
201 const response = await fetch('/api/openai/chutes/generate-voice', {
202 method: 'POST',
203 headers: getRequestHeaders(),
204 body: JSON.stringify({
205 input: text,
206 voice: voiceId || 'af_heart',
207 speed: this.settings.speed || 1,
208 }),
209 });
210
211 if (!response.ok) {
212 const error = await response.text();
213 throw new Error(`Chutes TTS failed: ${error}`);
214 }
215
216 return response;
217 }
218
219 async fetchTtsVoiceObjects() {
220 if (this.voices.length === 0) {
221 await this.updateVoices();
222 }
223
224 const voiceIds = this.voices
225 .map(voice => ({ name: voice.name, voice_id: voice.voice_id, preview_url: false }));
226 return voiceIds;
227 }
228
229 async previewTtsVoice(voiceId) {
230 const text = getPreviewString(voiceId);
231 await this.generateTts(text, voiceId);
232 }
233}