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

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

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

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

14
15
16
	let OpenAIUrl = '';
	let OpenAIKey = '';

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

Timothy J. Baek's avatar
Timothy J. Baek committed
20
	let conversationMode = false;
Timothy J. Baek's avatar
Timothy J. Baek committed
21
22
23
	let speechAutoSend = false;
	let responseAutoPlayback = false;

Timothy J. Baek's avatar
Timothy J. Baek committed
24
25
	let TTSEngines = ['', 'openai'];
	let TTSEngine = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
26
27
28

	let voices = [];
	let speaker = '';
29
	let models = [];
Yanyutin753's avatar
Yanyutin753 committed
30
	let model = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
31

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

43
44
45
46
	const getOpenAIVoicesModel = () => {
		models = [{ name: 'tts-1' }, { name: 'tts-1-hd' }];
	};

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

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

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

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

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

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

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

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

			if (res) {
				OpenAIUrl = res.OPENAI_API_BASE_URL;
				OpenAIKey = res.OPENAI_API_KEY;
Yanyutin753's avatar
Yanyutin753 committed
95
96
				model = res.OPENAI_API_MODEL;
				speaker = res.OPENAI_API_VOICE;
Timothy J. Baek's avatar
Timothy J. Baek committed
97
			}
98
99
100
		}
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
101
102
103
	onMount(async () => {
		let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');

Timothy J. Baek's avatar
Timothy J. Baek committed
104
		conversationMode = settings.conversationMode ?? false;
Timothy J. Baek's avatar
Timothy J. Baek committed
105
106
107
		speechAutoSend = settings.speechAutoSend ?? false;
		responseAutoPlayback = settings.responseAutoPlayback ?? false;

Timothy J. Baek's avatar
Timothy J. Baek committed
108
109
110
		STTEngine = settings?.audio?.STTEngine ?? '';
		TTSEngine = settings?.audio?.TTSEngine ?? '';
		speaker = settings?.audio?.speaker ?? '';
Yanyutin753's avatar
Yanyutin753 committed
111
		model = settings?.audio?.model ?? '';
Timothy J. Baek's avatar
Timothy J. Baek committed
112

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

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

Timothy J. Baek's avatar
Timothy J. Baek committed
123
124
125
			if (res) {
				OpenAIUrl = res.OPENAI_API_BASE_URL;
				OpenAIKey = res.OPENAI_API_KEY;
Yanyutin753's avatar
Yanyutin753 committed
126
127
				model = res.OPENAI_API_MODEL;
				speaker = res.OPENAI_API_VOICE;
Timothy J. Baek's avatar
Timothy J. Baek committed
128
			}
129
		}
Timothy J. Baek's avatar
Timothy J. Baek committed
130
131
132
133
134
	});
</script>

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

			<div class=" py-0.5 flex w-full justify-between">
155
				<div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
156
157
				<div class="flex items-center relative">
					<select
Jannik Streidl's avatar
Jannik Streidl committed
158
						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
159
						bind:value={STTEngine}
Timothy J. Baek's avatar
Timothy J. Baek committed
160
161
						placeholder="Select a mode"
						on:change={(e) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
162
163
							if (e.target.value !== '') {
								navigator.mediaDevices.getUserMedia({ audio: true }).catch(function (err) {
Ased Mammad's avatar
Ased Mammad committed
164
165
166
167
168
									toast.error(
										$i18n.t(`Permission denied when accessing microphone: {{error}}`, {
											error: err
										})
									);
Timothy J. Baek's avatar
Timothy J. Baek committed
169
170
									STTEngine = '';
								});
Timothy J. Baek's avatar
Timothy J. Baek committed
171
172
173
							}
						}}
					>
174
175
						<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
176
177
178
179
180
					</select>
				</div>
			</div>

			<div class=" py-0.5 flex w-full justify-between">
181
				<div class=" self-center text-xs font-medium">{$i18n.t('Conversation Mode')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
182
183
184
185
186
187
188
189
190

				<button
					class="p-1 px-3 text-xs flex rounded transition"
					on:click={() => {
						toggleConversationMode();
					}}
					type="button"
				>
					{#if conversationMode === true}
191
						<span class="ml-2 self-center">{$i18n.t('On')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
192
					{:else}
193
						<span class="ml-2 self-center">{$i18n.t('Off')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
194
195
196
197
198
					{/if}
				</button>
			</div>

			<div class=" py-0.5 flex w-full justify-between">
199
200
201
				<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
202
203
204
205
206
207
208
209
210

				<button
					class="p-1 px-3 text-xs flex rounded transition"
					on:click={() => {
						toggleSpeechAutoSend();
					}}
					type="button"
				>
					{#if speechAutoSend === true}
211
						<span class="ml-2 self-center">{$i18n.t('On')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
212
					{:else}
213
						<span class="ml-2 self-center">{$i18n.t('Off')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
214
215
216
					{/if}
				</button>
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
217
218
219
		</div>

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

			<div class=" py-0.5 flex w-full justify-between">
223
				<div class=" self-center text-xs font-medium">{$i18n.t('Text-to-Speech Engine')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
224
225
				<div class="flex items-center relative">
					<select
Jannik Streidl's avatar
Jannik Streidl committed
226
						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
227
228
229
230
231
232
						bind:value={TTSEngine}
						placeholder="Select a mode"
						on:change={(e) => {
							if (e.target.value === 'openai') {
								getOpenAIVoices();
								speaker = 'alloy';
Yanyutin753's avatar
Yanyutin753 committed
233
								model = 'tts-1';
Timothy J. Baek's avatar
Timothy J. Baek committed
234
235
236
237
238
239
							} else {
								getWebAPIVoices();
								speaker = '';
							}
						}}
					>
240
241
						<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
242
243
244
					</select>
				</div>
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
245

Timothy J. Baek's avatar
Timothy J. Baek committed
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
			{#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}
264
265
			{/if}

Timothy J. Baek's avatar
Timothy J. Baek committed
266
			<div class=" py-0.5 flex w-full justify-between">
267
				<div class=" self-center text-xs font-medium">{$i18n.t('Auto-playback response')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
268
269
270
271
272

				<button
					class="p-1 px-3 text-xs flex rounded transition"
					on:click={() => {
						toggleResponseAutoPlayback();
Timothy J. Baek's avatar
Timothy J. Baek committed
273
					}}
Timothy J. Baek's avatar
Timothy J. Baek committed
274
					type="button"
Timothy J. Baek's avatar
Timothy J. Baek committed
275
				>
Timothy J. Baek's avatar
Timothy J. Baek committed
276
					{#if responseAutoPlayback === true}
277
						<span class="ml-2 self-center">{$i18n.t('On')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
278
					{:else}
279
						<span class="ml-2 self-center">{$i18n.t('Off')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
280
281
					{/if}
				</button>
Timothy J. Baek's avatar
Timothy J. Baek committed
282
283
284
285
286
			</div>
		</div>

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

Timothy J. Baek's avatar
Timothy J. Baek committed
287
		{#if TTSEngine === ''}
Timothy J. Baek's avatar
Timothy J. Baek committed
288
			<div>
289
				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
290
291
292
				<div class="flex w-full">
					<div class="flex-1">
						<select
Timothy J. Baek's avatar
Timothy J. Baek committed
293
							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
294
							bind:value={speaker}
Timothy J. Baek's avatar
Timothy J. Baek committed
295
296
							placeholder="Select a voice"
						>
Ased Mammad's avatar
Ased Mammad committed
297
							<option value="" selected>{$i18n.t('Default')}</option>
Timothy J. Baek's avatar
Timothy J. Baek committed
298
							{#each voices.filter((v) => v.localService === true) as voice}
Timothy J. Baek's avatar
Timothy J. Baek committed
299
300
301
302
303
304
305
								<option value={voice.name} class="bg-gray-100 dark:bg-gray-700">{voice.name}</option
								>
							{/each}
						</select>
					</div>
				</div>
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
306
		{:else if TTSEngine === 'openai'}
Timothy J. Baek's avatar
Timothy J. Baek committed
307
			<div>
308
				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
309
310
				<div class="flex w-full">
					<div class="flex-1">
311
312
313
						<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"
Timothy J. Baek's avatar
Timothy J. Baek committed
314
315
							bind:value={speaker}
							placeholder="Select a voice"
316
317
318
						/>

						<datalist id="voice-list">
Timothy J. Baek's avatar
Timothy J. Baek committed
319
							{#each voices as voice}
320
								<option value={voice.name} />
Timothy J. Baek's avatar
Timothy J. Baek committed
321
							{/each}
322
						</datalist>
Timothy J. Baek's avatar
Timothy J. Baek committed
323
324
325
					</div>
				</div>
			</div>
326
327
328
329
330
331
332
			<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
333
							bind:value={model}
334
335
336
337
							placeholder="Select a model"
						/>

						<datalist id="model-list">
Yanyutin753's avatar
Yanyutin753 committed
338
339
							{#each models as model}
								<option value={model.name} />
340
341
342
343
344
							{/each}
						</datalist>
					</div>
				</div>
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
345
		{/if}
Timothy J. Baek's avatar
Timothy J. Baek committed
346
347
	</div>

Timothy J. Baek's avatar
Timothy J. Baek committed
348
	<div class="flex justify-end text-sm font-medium">
Timothy J. Baek's avatar
Timothy J. Baek committed
349
		<button
Timothy J. Baek's avatar
Timothy J. Baek committed
350
			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
351
352
			type="submit"
		>
Jannik Streidl's avatar
Jannik Streidl committed
353
			{$i18n.t('Save')}
Timothy J. Baek's avatar
Timothy J. Baek committed
354
355
356
		</button>
	</div>
</form>