Audio.svelte 10.7 KB
Newer Older
Timothy J. Baek's avatar
Timothy J. Baek committed
1
2
3
4
5
6
7
<script lang="ts">
	import { getAudioConfig, updateAudioConfig } from '$lib/apis/audio';
	import { user, settings, config } from '$lib/stores';
	import { createEventDispatcher, onMount, getContext } from 'svelte';
	import { toast } from 'svelte-sonner';
	import Switch from '$lib/components/common/Switch.svelte';
	import { getBackendConfig } from '$lib/apis';
8
	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
Timothy J. Baek's avatar
Timothy J. Baek committed
9
10
11
12
13
14
15
16
17
18
	const dispatch = createEventDispatcher();

	const i18n = getContext('i18n');

	export let saveHandler: Function;

	// Audio

	let TTS_OPENAI_API_BASE_URL = '';
	let TTS_OPENAI_API_KEY = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
19
	let TTS_API_KEY = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
	let TTS_ENGINE = '';
	let TTS_MODEL = '';
	let TTS_VOICE = '';

	let STT_OPENAI_API_BASE_URL = '';
	let STT_OPENAI_API_KEY = '';
	let STT_ENGINE = '';
	let STT_MODEL = '';

	let voices = [];
	let models = [];
	let nonLocalVoices = false;

	const getOpenAIVoices = () => {
		voices = [
			{ name: 'alloy' },
			{ name: 'echo' },
			{ name: 'fable' },
			{ name: 'onyx' },
			{ name: 'nova' },
			{ name: 'shimmer' }
		];
	};

	const getOpenAIModels = () => {
		models = [{ name: 'tts-1' }, { name: 'tts-1-hd' }];
	};

	const getWebAPIVoices = () => {
		const getVoicesLoop = setInterval(async () => {
			voices = await speechSynthesis.getVoices();

			// do your loop
			if (voices.length > 0) {
				clearInterval(getVoicesLoop);
			}
		}, 100);
	};

59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
    // Fetch available ElevenLabs voices
    const fetchAvailableVoices = async () => {
        const response = await fetch('/voices', {
            method: 'GET',
            headers: {
                'Authorization': `Bearer ${localStorage.token}`
            }
        });

        if (response.ok) {
            const data = await response.json();
            voices = data.voices.map(name => ({ name })); // Update voices array with fetched names
        } else {
            toast.error('Failed to fetch voices');
        }
    };

Timothy J. Baek's avatar
Timothy J. Baek committed
76
77
78
79
80
	const updateConfigHandler = async () => {
		const res = await updateAudioConfig(localStorage.token, {
			tts: {
				OPENAI_API_BASE_URL: TTS_OPENAI_API_BASE_URL,
				OPENAI_API_KEY: TTS_OPENAI_API_KEY,
Timothy J. Baek's avatar
Timothy J. Baek committed
81
				API_KEY: TTS_API_KEY,
Timothy J. Baek's avatar
Timothy J. Baek committed
82
83
84
85
86
87
88
89
90
91
92
93
94
				ENGINE: TTS_ENGINE,
				MODEL: TTS_MODEL,
				VOICE: TTS_VOICE
			},
			stt: {
				OPENAI_API_BASE_URL: STT_OPENAI_API_BASE_URL,
				OPENAI_API_KEY: STT_OPENAI_API_KEY,
				ENGINE: STT_ENGINE,
				MODEL: STT_MODEL
			}
		});

		if (res) {
SimonOriginal's avatar
SimonOriginal committed
95
			toast.success($i18n.t('Audio settings updated successfully'));
Timothy J. Baek's avatar
Timothy J. Baek committed
96
97
98
99
100
101

			config.set(await getBackendConfig());
		}
	};

	onMount(async () => {
102
103
104
        // Fetch available voices on component mount
        await fetchAvailableVoices(); 
        
Timothy J. Baek's avatar
Timothy J. Baek committed
105
106
107
108
109
110
		const res = await getAudioConfig(localStorage.token);

		if (res) {
			console.log(res);
			TTS_OPENAI_API_BASE_URL = res.tts.OPENAI_API_BASE_URL;
			TTS_OPENAI_API_KEY = res.tts.OPENAI_API_KEY;
Timothy J. Baek's avatar
Timothy J. Baek committed
111
			TTS_API_KEY = res.tts.API_KEY;
Timothy J. Baek's avatar
Timothy J. Baek committed
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126

			TTS_ENGINE = res.tts.ENGINE;
			TTS_MODEL = res.tts.MODEL;
			TTS_VOICE = res.tts.VOICE;

			STT_OPENAI_API_BASE_URL = res.stt.OPENAI_API_BASE_URL;
			STT_OPENAI_API_KEY = res.stt.OPENAI_API_KEY;

			STT_ENGINE = res.stt.ENGINE;
			STT_MODEL = res.stt.MODEL;
		}

		if (TTS_ENGINE === 'openai') {
			getOpenAIVoices();
			getOpenAIModels();
127
128
        } else if(TTS_ENGINE === 'elevenlabs') {
            await fetchAvailableVoices(); // Fetch voices if TTS_ENGINE is ElevenLabs
Timothy J. Baek's avatar
Timothy J. Baek committed
129
130
131
132
133
134
135
136
137
138
139
140
		} else {
			getWebAPIVoices();
		}
	});
</script>

<form
	class="flex flex-col h-full justify-between space-y-3 text-sm"
	on:submit|preventDefault={async () => {
		await updateConfigHandler();
		dispatch('save');
	}}
Justin Hayes's avatar
name  
Justin Hayes committed
141
>
Timothy J. Baek's avatar
Timothy J. Baek committed
142
	<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
Timothy J. Baek's avatar
Timothy J. Baek committed
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
		<div class="flex flex-col gap-3">
			<div>
				<div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div>

				<div class=" py-0.5 flex w-full justify-between">
					<div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div>
					<div class="flex items-center relative">
						<select
							class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
							bind:value={STT_ENGINE}
							placeholder="Select an engine"
						>
							<option value="">{$i18n.t('Whisper (Local)')}</option>
							<option value="openai">OpenAI</option>
							<option value="web">{$i18n.t('Web API')}</option>
						</select>
Timothy J. Baek's avatar
Timothy J. Baek committed
159
160
161
					</div>
				</div>

Timothy J. Baek's avatar
Timothy J. Baek committed
162
163
164
				{#if STT_ENGINE === 'openai'}
					<div>
						<div class="mt-1 flex gap-2 mb-1">
Timothy J. Baek's avatar
Timothy J. Baek committed
165
							<input
Timothy J. Baek's avatar
Timothy J. Baek committed
166
								class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
Timothy J. Baek's avatar
Timothy J. Baek committed
167
168
169
								placeholder={$i18n.t('API Base URL')}
								bind:value={STT_OPENAI_API_BASE_URL}
								required
Timothy J. Baek's avatar
Timothy J. Baek committed
170
171
							/>

172
							<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={STT_OPENAI_API_KEY} />
Timothy J. Baek's avatar
Timothy J. Baek committed
173
174
175
						</div>
					</div>

Timothy J. Baek's avatar
Timothy J. Baek committed
176
177
178
179
180
181
182
183
					<hr class=" dark:border-gray-850 my-2" />

					<div>
						<div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div>
						<div class="flex w-full">
							<div class="flex-1">
								<input
									list="model-list"
Timothy J. Baek's avatar
Timothy J. Baek committed
184
									class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
Timothy J. Baek's avatar
Timothy J. Baek committed
185
186
187
188
189
190
191
192
193
194
195
									bind:value={STT_MODEL}
									placeholder="Select a model"
								/>

								<datalist id="model-list">
									<option value="whisper-1" />
								</datalist>
							</div>
						</div>
					</div>
				{/if}
Timothy J. Baek's avatar
Timothy J. Baek committed
196
197
			</div>

Timothy J. Baek's avatar
Timothy J. Baek committed
198
199
200
201
202
203
204
205
206
207
208
209
			<hr class=" dark:border-gray-800" />

			<div>
				<div class=" mb-1 text-sm font-medium">{$i18n.t('TTS Settings')}</div>

				<div class=" py-0.5 flex w-full justify-between">
					<div class=" self-center text-xs font-medium">{$i18n.t('Text-to-Speech Engine')}</div>
					<div class="flex items-center relative">
						<select
							class=" dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
							bind:value={TTS_ENGINE}
							placeholder="Select a mode"
210
							on:change={async (e) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
211
212
213
214
								if (e.target.value === 'openai') {
									getOpenAIVoices();
									TTS_VOICE = 'alloy';
									TTS_MODEL = 'tts-1';
215
216
								} else if(e.target.value === 'elevenlabs') {
									await fetchAvailableVoices();
Timothy J. Baek's avatar
Timothy J. Baek committed
217
218
219
								} else {
									getWebAPIVoices();
									TTS_VOICE = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
220
									TTS_MODEL = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
221
222
223
224
								}
							}}
						>
							<option value="">{$i18n.t('Web API')}</option>
225
							<option value="openai">{$i18n.t('OpenAI')}</option>
Justin Hayes's avatar
name  
Justin Hayes committed
226
							<option value="elevenlabs">{$i18n.t('ElevenLabs')}</option>
Timothy J. Baek's avatar
Timothy J. Baek committed
227
						</select>
Timothy J. Baek's avatar
Timothy J. Baek committed
228
229
230
					</div>
				</div>

Timothy J. Baek's avatar
Timothy J. Baek committed
231
232
233
234
				{#if TTS_ENGINE === 'openai'}
					<div>
						<div class="mt-1 flex gap-2 mb-1">
							<input
Timothy J. Baek's avatar
Timothy J. Baek committed
235
								class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
Timothy J. Baek's avatar
Timothy J. Baek committed
236
237
238
239
								placeholder={$i18n.t('API Base URL')}
								bind:value={TTS_OPENAI_API_BASE_URL}
								required
							/>
Timothy J. Baek's avatar
Timothy J. Baek committed
240

241
							<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={TTS_OPENAI_API_KEY} />
Timothy J. Baek's avatar
Timothy J. Baek committed
242
243
						</div>
					</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
244
245
246
247
248
249
250
251
252
253
254
				{:else if TTS_ENGINE === 'elevenlabs'}
					<div>
						<div class="mt-1 flex gap-2 mb-1">
							<input
								class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
								placeholder={$i18n.t('API Key')}
								bind:value={TTS_API_KEY}
								required
							/>
						</div>
					</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
255
256
257
258
				{/if}

				<hr class=" dark:border-gray-850 my-2" />

259
				{#if TTS_ENGINE !== ''}
Timothy J. Baek's avatar
Timothy J. Baek committed
260
					<div>
Timothy J. Baek's avatar
Timothy J. Baek committed
261
262
263
						<div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Voice')}</div>
						<div class="flex w-full">
							<div class="flex-1">
Timothy J. Baek's avatar
Timothy J. Baek committed
264
								<select
Timothy J. Baek's avatar
Timothy J. Baek committed
265
									class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
Timothy J. Baek's avatar
Timothy J. Baek committed
266
									bind:value={TTS_VOICE}
Timothy J. Baek's avatar
Timothy J. Baek committed
267
268
								>
									<option value="" selected={TTS_VOICE !== ''}>{$i18n.t('Default')}</option>
Timothy J. Baek's avatar
Timothy J. Baek committed
269
									{#each voices as voice}
Timothy J. Baek's avatar
Timothy J. Baek committed
270
										<option
271
											value={voice.name}
Timothy J. Baek's avatar
Timothy J. Baek committed
272
											class="bg-gray-100 dark:bg-gray-700"
273
											selected={TTS_VOICE === voice.name}>{voice.name}</option
Timothy J. Baek's avatar
Timothy J. Baek committed
274
										>
Timothy J. Baek's avatar
Timothy J. Baek committed
275
									{/each}
Timothy J. Baek's avatar
Timothy J. Baek committed
276
								</select>
Timothy J. Baek's avatar
Timothy J. Baek committed
277
278
279
							</div>
						</div>
					</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
280
				{:else if TTS_ENGINE === 'openai'}
Timothy J. Baek's avatar
Timothy J. Baek committed
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
					<div class=" flex gap-2">
						<div class="w-full">
							<div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Voice')}</div>
							<div class="flex w-full">
								<div class="flex-1">
									<input
										list="voice-list"
										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
										bind:value={TTS_VOICE}
										placeholder="Select a voice"
									/>

									<datalist id="voice-list">
										{#each voices as voice}
											<option value={voice.name} />
										{/each}
									</datalist>
								</div>
							</div>
						</div>
						<div class="w-full">
							<div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Model')}</div>
							<div class="flex w-full">
								<div class="flex-1">
									<input
										list="model-list"
										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
										bind:value={TTS_MODEL}
										placeholder="Select a model"
									/>

									<datalist id="model-list">
										{#each models as model}
											<option value={model.name} />
										{/each}
									</datalist>
								</div>
							</div>
						</div>
					</div>
				{:else if TTS_ENGINE === 'elevenlabs'}
Timothy J. Baek's avatar
Timothy J. Baek committed
322
323
324
325
326
327
328
					<div class=" flex gap-2">
						<div class="w-full">
							<div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Voice')}</div>
							<div class="flex w-full">
								<div class="flex-1">
									<input
										list="voice-list"
Timothy J. Baek's avatar
Timothy J. Baek committed
329
										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
Timothy J. Baek's avatar
Timothy J. Baek committed
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
										bind:value={TTS_VOICE}
										placeholder="Select a voice"
									/>

									<datalist id="voice-list">
										{#each voices as voice}
											<option value={voice.name} />
										{/each}
									</datalist>
								</div>
							</div>
						</div>
						<div class="w-full">
							<div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Model')}</div>
							<div class="flex w-full">
								<div class="flex-1">
									<input
										list="model-list"
Timothy J. Baek's avatar
Timothy J. Baek committed
348
										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
Timothy J. Baek's avatar
Timothy J. Baek committed
349
350
351
352
353
354
355
356
357
358
										bind:value={TTS_MODEL}
										placeholder="Select a model"
									/>

									<datalist id="model-list">
										{#each models as model}
											<option value={model.name} />
										{/each}
									</datalist>
								</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
359
360
361
							</div>
						</div>
					</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
362
363
				{/if}
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
364
365
366
367
368
369
370
371
372
373
374
		</div>
	</div>
	<div class="flex justify-end text-sm font-medium">
		<button
			class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
			type="submit"
		>
			{$i18n.t('Save')}
		</button>
	</div>
</form>