Audio.svelte 9.74 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
17
	let OpenAIUrl = '';
	let OpenAIKey = '';

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Timothy J. Baek's avatar
Timothy J. Baek committed
103
	onMount(async () => {
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 ?? '';
110
		nonLocalVoices = $settings.audio?.nonLocalVoices ?? false;
111
112
		speaker = $settings?.audio?.speaker ?? '';
		model = $settings?.audio?.model ?? '';
Timothy J. Baek's avatar
Timothy J. Baek committed
113

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Timothy J. Baek's avatar
Timothy J. Baek committed
289
		{#if TTSEngine === ''}
Timothy J. Baek's avatar
Timothy J. Baek committed
290
			<div>
291
				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
292
293
294
				<div class="flex w-full">
					<div class="flex-1">
						<select
Timothy J. Baek's avatar
Timothy J. Baek committed
295
							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
296
							bind:value={speaker}
Timothy J. Baek's avatar
Timothy J. Baek committed
297
						>
298
299
300
301
302
303
							<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
304
305
306
307
308
								>
							{/each}
						</select>
					</div>
				</div>
309
310
311
312
313
314
315
316
317
				<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
318
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
319
		{:else if TTSEngine === 'openai'}
Timothy J. Baek's avatar
Timothy J. Baek committed
320
			<div>
321
				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
322
323
				<div class="flex w-full">
					<div class="flex-1">
324
325
326
						<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
327
328
							bind:value={speaker}
							placeholder="Select a voice"
329
330
331
						/>

						<datalist id="voice-list">
Timothy J. Baek's avatar
Timothy J. Baek committed
332
							{#each voices as voice}
333
								<option value={voice.name} />
Timothy J. Baek's avatar
Timothy J. Baek committed
334
							{/each}
335
						</datalist>
Timothy J. Baek's avatar
Timothy J. Baek committed
336
337
338
					</div>
				</div>
			</div>
339
340
341
342
343
344
345
			<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
346
							bind:value={model}
347
348
349
350
							placeholder="Select a model"
						/>

						<datalist id="model-list">
Yanyutin753's avatar
Yanyutin753 committed
351
352
							{#each models as model}
								<option value={model.name} />
353
354
355
356
357
							{/each}
						</datalist>
					</div>
				</div>
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
358
		{/if}
Timothy J. Baek's avatar
Timothy J. Baek committed
359
360
	</div>

Timothy J. Baek's avatar
Timothy J. Baek committed
361
	<div class="flex justify-end text-sm font-medium">
Timothy J. Baek's avatar
Timothy J. Baek committed
362
		<button
Timothy J. Baek's avatar
Timothy J. Baek committed
363
			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
364
365
			type="submit"
		>
Jannik Streidl's avatar
Jannik Streidl committed
366
			{$i18n.t('Save')}
Timothy J. Baek's avatar
Timothy J. Baek committed
367
368
369
		</button>
	</div>
</form>