Audio.svelte 9.27 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';
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
	onMount(async () => {
102
103
104
105
106
107
108
109
		conversationMode = $settings.conversationMode ?? false;
		speechAutoSend = $settings.speechAutoSend ?? false;
		responseAutoPlayback = $settings.responseAutoPlayback ?? false;

		STTEngine = $settings?.audio?.STTEngine ?? '';
		TTSEngine = $settings?.audio?.TTSEngine ?? '';
		speaker = $settings?.audio?.speaker ?? '';
		model = $settings?.audio?.model ?? '';
Timothy J. Baek's avatar
Timothy J. Baek committed
110

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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