Audio.svelte 9.97 KB
Newer Older
Timothy J. Baek's avatar
Timothy J. Baek committed
1
<script lang="ts">
2
	import { getAudioConfig, updateAudioConfig } from '$lib/apis/audio';
3
	import { user, settings } from '$lib/stores';
4
	import { createEventDispatcher, onMount, getContext } from 'svelte';
Jannik Streidl's avatar
Jannik Streidl committed
5
	import { toast } from 'svelte-sonner';
6
	import Switch from '$lib/components/common/Switch.svelte';
Timothy J. Baek's avatar
Timothy J. Baek committed
7
8
	const dispatch = createEventDispatcher();

9
10
	const i18n = getContext('i18n');

Timothy J. Baek's avatar
Timothy J. Baek committed
11
12
	export let saveSettings: Function;

Timothy J. Baek's avatar
Timothy J. Baek committed
13
	// Audio
Timothy J. Baek's avatar
Timothy J. Baek committed
14

15
16
	let OpenAIUrl = '';
	let OpenAIKey = '';
17
	let OpenAISpeaker = '';
18

Timothy J. Baek's avatar
Timothy J. Baek committed
19
20
21
	let STTEngines = ['', 'openai'];
	let STTEngine = '';

Timothy J. Baek's avatar
Timothy J. Baek committed
22
	let conversationMode = false;
Timothy J. Baek's avatar
Timothy J. Baek committed
23
24
	let speechAutoSend = false;
	let responseAutoPlayback = false;
25
	let nonLocalVoices = false;
Timothy J. Baek's avatar
Timothy J. Baek committed
26

Timothy J. Baek's avatar
Timothy J. Baek committed
27
28
	let TTSEngines = ['', 'openai'];
	let TTSEngine = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
29
30
31

	let voices = [];
	let speaker = '';
32
	let models = [];
Yanyutin753's avatar
Yanyutin753 committed
33
	let model = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
34

Timothy J. Baek's avatar
Timothy J. Baek committed
35
36
37
38
39
40
41
42
43
44
	const getOpenAIVoices = () => {
		voices = [
			{ name: 'alloy' },
			{ name: 'echo' },
			{ name: 'fable' },
			{ name: 'onyx' },
			{ name: 'nova' },
			{ name: 'shimmer' }
		];
	};
Timothy J. Baek's avatar
Timothy J. Baek committed
45

46
47
48
49
	const getOpenAIVoicesModel = () => {
		models = [{ name: 'tts-1' }, { name: 'tts-1-hd' }];
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
50
	const getWebAPIVoices = () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
51
		const getVoicesLoop = setInterval(async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
52
			voices = await speechSynthesis.getVoices();
Timothy J. Baek's avatar
Timothy J. Baek committed
53
54

			// do your loop
Timothy J. Baek's avatar
Timothy J. Baek committed
55
			if (voices.length > 0) {
Timothy J. Baek's avatar
Timothy J. Baek committed
56
57
58
				clearInterval(getVoicesLoop);
			}
		}, 100);
Timothy J. Baek's avatar
Timothy J. Baek committed
59
60
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
61
62
	const toggleConversationMode = async () => {
		conversationMode = !conversationMode;
Timothy J. Baek's avatar
Timothy J. Baek committed
63
64
65
66
67
68
69
70
71
72
73

		if (conversationMode) {
			responseAutoPlayback = true;
			speechAutoSend = true;
		}

		saveSettings({
			conversationMode: conversationMode,
			responseAutoPlayback: responseAutoPlayback,
			speechAutoSend: speechAutoSend
		});
Timothy J. Baek's avatar
Timothy J. Baek committed
74
75
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
76
77
78
79
80
81
82
83
84
85
	const toggleResponseAutoPlayback = async () => {
		responseAutoPlayback = !responseAutoPlayback;
		saveSettings({ responseAutoPlayback: responseAutoPlayback });
	};

	const toggleSpeechAutoSend = async () => {
		speechAutoSend = !speechAutoSend;
		saveSettings({ speechAutoSend: speechAutoSend });
	};

86
	const updateConfigHandler = async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
87
88
89
		if (TTSEngine === 'openai') {
			const res = await updateAudioConfig(localStorage.token, {
				url: OpenAIUrl,
90
				key: OpenAIKey,
Yanyutin753's avatar
Yanyutin753 committed
91
				model: model,
92
				speaker: OpenAISpeaker
Timothy J. Baek's avatar
Timothy J. Baek committed
93
94
95
96
97
			});

			if (res) {
				OpenAIUrl = res.OPENAI_API_BASE_URL;
				OpenAIKey = res.OPENAI_API_KEY;
Yanyutin753's avatar
Yanyutin753 committed
98
				model = res.OPENAI_API_MODEL;
99
				OpenAISpeaker = res.OPENAI_API_VOICE;
Timothy J. Baek's avatar
Timothy J. Baek committed
100
			}
101
102
103
		}
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
104
	onMount(async () => {
105
106
107
108
109
110
		conversationMode = $settings.conversationMode ?? false;
		speechAutoSend = $settings.speechAutoSend ?? false;
		responseAutoPlayback = $settings.responseAutoPlayback ?? false;

		STTEngine = $settings?.audio?.STTEngine ?? '';
		TTSEngine = $settings?.audio?.TTSEngine ?? '';
111
		nonLocalVoices = $settings.audio?.nonLocalVoices ?? false;
112
113
		speaker = $settings?.audio?.speaker ?? '';
		model = $settings?.audio?.model ?? '';
Timothy J. Baek's avatar
Timothy J. Baek committed
114

Timothy J. Baek's avatar
Timothy J. Baek committed
115
		if (TTSEngine === 'openai') {
Timothy J. Baek's avatar
Timothy J. Baek committed
116
			getOpenAIVoices();
117
			getOpenAIVoicesModel();
Timothy J. Baek's avatar
Timothy J. Baek committed
118
119
120
		} else {
			getWebAPIVoices();
		}
121

Timothy J. Baek's avatar
Timothy J. Baek committed
122
123
		if ($user.role === 'admin') {
			const res = await getAudioConfig(localStorage.token);
124

Timothy J. Baek's avatar
Timothy J. Baek committed
125
126
127
			if (res) {
				OpenAIUrl = res.OPENAI_API_BASE_URL;
				OpenAIKey = res.OPENAI_API_KEY;
Yanyutin753's avatar
Yanyutin753 committed
128
				model = res.OPENAI_API_MODEL;
129
130
131
132
				OpenAISpeaker = res.OPENAI_API_VOICE;
				if (TTSEngine === 'openai') {
					speaker = OpenAISpeaker;
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
133
			}
134
		}
Timothy J. Baek's avatar
Timothy J. Baek committed
135
136
137
138
139
	});
</script>

<form
	class="flex flex-col h-full justify-between space-y-3 text-sm"
140
	on:submit|preventDefault={async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
141
142
143
		if ($user.role === 'admin') {
			await updateConfigHandler();
		}
Timothy J. Baek's avatar
Timothy J. Baek committed
144
		saveSettings({
Timothy J. Baek's avatar
Timothy J. Baek committed
145
			audio: {
Timothy J. Baek's avatar
Timothy J. Baek committed
146
147
				STTEngine: STTEngine !== '' ? STTEngine : undefined,
				TTSEngine: TTSEngine !== '' ? TTSEngine : undefined,
148
149
150
151
152
153
				speaker:
					(TTSEngine === 'openai' ? OpenAISpeaker : speaker) !== ''
						? TTSEngine === 'openai'
							? OpenAISpeaker
							: speaker
						: undefined,
154
155
				model: model !== '' ? model : undefined,
				nonLocalVoices: nonLocalVoices
Timothy J. Baek's avatar
Timothy J. Baek committed
156
			}
Timothy J. Baek's avatar
Timothy J. Baek committed
157
158
159
160
		});
		dispatch('save');
	}}
>
Timothy J. Baek's avatar
Timothy J. Baek committed
161
	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem]">
Timothy J. Baek's avatar
Timothy J. Baek committed
162
		<div>
163
			<div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
164
165

			<div class=" py-0.5 flex w-full justify-between">
166
				<div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
167
168
				<div class="flex items-center relative">
					<select
Jannik Streidl's avatar
Jannik Streidl committed
169
						class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
Timothy J. Baek's avatar
Timothy J. Baek committed
170
						bind:value={STTEngine}
Timothy J. Baek's avatar
Timothy J. Baek committed
171
172
						placeholder="Select a mode"
						on:change={(e) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
173
174
							if (e.target.value !== '') {
								navigator.mediaDevices.getUserMedia({ audio: true }).catch(function (err) {
Ased Mammad's avatar
Ased Mammad committed
175
176
177
178
179
									toast.error(
										$i18n.t(`Permission denied when accessing microphone: {{error}}`, {
											error: err
										})
									);
Timothy J. Baek's avatar
Timothy J. Baek committed
180
181
									STTEngine = '';
								});
Timothy J. Baek's avatar
Timothy J. Baek committed
182
183
184
							}
						}}
					>
185
186
						<option value="">{$i18n.t('Default (Web API)')}</option>
						<option value="whisper-local">{$i18n.t('Whisper (Local)')}</option>
Timothy J. Baek's avatar
Timothy J. Baek committed
187
188
189
190
191
					</select>
				</div>
			</div>

			<div class=" py-0.5 flex w-full justify-between">
192
				<div class=" self-center text-xs font-medium">{$i18n.t('Conversation Mode')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
193
194
195
196
197
198
199
200
201

				<button
					class="p-1 px-3 text-xs flex rounded transition"
					on:click={() => {
						toggleConversationMode();
					}}
					type="button"
				>
					{#if conversationMode === true}
202
						<span class="ml-2 self-center">{$i18n.t('On')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
203
					{:else}
204
						<span class="ml-2 self-center">{$i18n.t('Off')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
205
206
207
208
209
					{/if}
				</button>
			</div>

			<div class=" py-0.5 flex w-full justify-between">
210
211
212
				<div class=" self-center text-xs font-medium">
					{$i18n.t('Auto-send input after 3 sec.')}
				</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
213
214
215
216
217
218
219
220
221

				<button
					class="p-1 px-3 text-xs flex rounded transition"
					on:click={() => {
						toggleSpeechAutoSend();
					}}
					type="button"
				>
					{#if speechAutoSend === true}
222
						<span class="ml-2 self-center">{$i18n.t('On')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
223
					{:else}
224
						<span class="ml-2 self-center">{$i18n.t('Off')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
225
226
227
					{/if}
				</button>
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
228
229
230
		</div>

		<div>
231
			<div class=" mb-1 text-sm font-medium">{$i18n.t('TTS Settings')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
232
233

			<div class=" py-0.5 flex w-full justify-between">
234
				<div class=" self-center text-xs font-medium">{$i18n.t('Text-to-Speech Engine')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
235
236
				<div class="flex items-center relative">
					<select
Jannik Streidl's avatar
Jannik Streidl committed
237
						class=" dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
Timothy J. Baek's avatar
Timothy J. Baek committed
238
239
240
241
242
						bind:value={TTSEngine}
						placeholder="Select a mode"
						on:change={(e) => {
							if (e.target.value === 'openai') {
								getOpenAIVoices();
243
								OpenAISpeaker = 'alloy';
Yanyutin753's avatar
Yanyutin753 committed
244
								model = 'tts-1';
Timothy J. Baek's avatar
Timothy J. Baek committed
245
246
247
248
249
250
							} else {
								getWebAPIVoices();
								speaker = '';
							}
						}}
					>
251
252
						<option value="">{$i18n.t('Default (Web API)')}</option>
						<option value="openai">{$i18n.t('Open AI')}</option>
Timothy J. Baek's avatar
Timothy J. Baek committed
253
254
255
					</select>
				</div>
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
256

Timothy J. Baek's avatar
Timothy J. Baek committed
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
			{#if $user.role === 'admin'}
				{#if TTSEngine === 'openai'}
					<div class="mt-1 flex gap-2 mb-1">
						<input
							class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
							placeholder={$i18n.t('API Base URL')}
							bind:value={OpenAIUrl}
							required
						/>

						<input
							class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
							placeholder={$i18n.t('API Key')}
							bind:value={OpenAIKey}
							required
						/>
					</div>
				{/if}
275
276
			{/if}

Timothy J. Baek's avatar
Timothy J. Baek committed
277
			<div class=" py-0.5 flex w-full justify-between">
278
				<div class=" self-center text-xs font-medium">{$i18n.t('Auto-playback response')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
279
280
281
282
283

				<button
					class="p-1 px-3 text-xs flex rounded transition"
					on:click={() => {
						toggleResponseAutoPlayback();
Timothy J. Baek's avatar
Timothy J. Baek committed
284
					}}
Timothy J. Baek's avatar
Timothy J. Baek committed
285
					type="button"
Timothy J. Baek's avatar
Timothy J. Baek committed
286
				>
Timothy J. Baek's avatar
Timothy J. Baek committed
287
					{#if responseAutoPlayback === true}
288
						<span class="ml-2 self-center">{$i18n.t('On')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
289
					{:else}
290
						<span class="ml-2 self-center">{$i18n.t('Off')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
291
292
					{/if}
				</button>
Timothy J. Baek's avatar
Timothy J. Baek committed
293
294
295
296
297
			</div>
		</div>

		<hr class=" dark:border-gray-700" />

Timothy J. Baek's avatar
Timothy J. Baek committed
298
		{#if TTSEngine === ''}
Timothy J. Baek's avatar
Timothy J. Baek committed
299
			<div>
300
				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
301
302
303
				<div class="flex w-full">
					<div class="flex-1">
						<select
Timothy J. Baek's avatar
Timothy J. Baek committed
304
							class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
Timothy J. Baek's avatar
Timothy J. Baek committed
305
							bind:value={speaker}
Timothy J. Baek's avatar
Timothy J. Baek committed
306
						>
307
308
309
310
311
312
							<option value="" selected={speaker !== ''}>{$i18n.t('Default')}</option>
							{#each voices.filter((v) => nonLocalVoices || v.localService === true) as voice}
								<option
									value={voice.name}
									class="bg-gray-100 dark:bg-gray-700"
									selected={speaker === voice.name}>{voice.name}</option
Timothy J. Baek's avatar
Timothy J. Baek committed
313
314
315
316
317
								>
							{/each}
						</select>
					</div>
				</div>
318
319
320
321
322
323
324
325
326
				<div class="flex items-center justify-between mb-1">
					<div class="text-sm">
						{$i18n.t('Allow non-local voices')}
					</div>

					<div class="mt-1">
						<Switch bind:state={nonLocalVoices} />
					</div>
				</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
327
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
328
		{:else if TTSEngine === 'openai'}
Timothy J. Baek's avatar
Timothy J. Baek committed
329
			<div>
330
				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
331
332
				<div class="flex w-full">
					<div class="flex-1">
333
334
335
						<input
							list="voice-list"
							class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
336
							bind:value={OpenAISpeaker}
Timothy J. Baek's avatar
Timothy J. Baek committed
337
							placeholder="Select a voice"
338
339
340
						/>

						<datalist id="voice-list">
Timothy J. Baek's avatar
Timothy J. Baek committed
341
							{#each voices as voice}
342
								<option value={voice.name} />
Timothy J. Baek's avatar
Timothy J. Baek committed
343
							{/each}
344
						</datalist>
Timothy J. Baek's avatar
Timothy J. Baek committed
345
346
347
					</div>
				</div>
			</div>
348
349
350
351
352
353
354
			<div>
				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set 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 dark:text-gray-300 dark:bg-gray-850 outline-none"
Yanyutin753's avatar
Yanyutin753 committed
355
							bind:value={model}
356
357
358
359
							placeholder="Select a model"
						/>

						<datalist id="model-list">
Yanyutin753's avatar
Yanyutin753 committed
360
361
							{#each models as model}
								<option value={model.name} />
362
363
364
365
366
							{/each}
						</datalist>
					</div>
				</div>
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
367
		{/if}
Timothy J. Baek's avatar
Timothy J. Baek committed
368
369
	</div>

Timothy J. Baek's avatar
Timothy J. Baek committed
370
	<div class="flex justify-end text-sm font-medium">
Timothy J. Baek's avatar
Timothy J. Baek committed
371
		<button
Timothy J. Baek's avatar
Timothy J. Baek committed
372
			class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
Timothy J. Baek's avatar
Timothy J. Baek committed
373
374
			type="submit"
		>
Jannik Streidl's avatar
Jannik Streidl committed
375
			{$i18n.t('Save')}
Timothy J. Baek's avatar
Timothy J. Baek committed
376
377
378
		</button>
	</div>
</form>