Audio.svelte 8.39 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 = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
29

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

Timothy J. Baek's avatar
Timothy J. Baek committed
41
	const getWebAPIVoices = () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
42
		const getVoicesLoop = setInterval(async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
43
			voices = await speechSynthesis.getVoices();
Timothy J. Baek's avatar
Timothy J. Baek committed
44
45

			// do your loop
Timothy J. Baek's avatar
Timothy J. Baek committed
46
			if (voices.length > 0) {
Timothy J. Baek's avatar
Timothy J. Baek committed
47
48
49
				clearInterval(getVoicesLoop);
			}
		}, 100);
Timothy J. Baek's avatar
Timothy J. Baek committed
50
51
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
52
53
	const toggleConversationMode = async () => {
		conversationMode = !conversationMode;
Timothy J. Baek's avatar
Timothy J. Baek committed
54
55
56
57
58
59
60
61
62
63
64

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

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

Timothy J. Baek's avatar
Timothy J. Baek committed
67
68
69
70
71
72
73
74
75
76
	const toggleResponseAutoPlayback = async () => {
		responseAutoPlayback = !responseAutoPlayback;
		saveSettings({ responseAutoPlayback: responseAutoPlayback });
	};

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

77
	const updateConfigHandler = async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
78
79
80
81
82
83
84
85
86
87
		if (TTSEngine === 'openai') {
			const res = await updateAudioConfig(localStorage.token, {
				url: OpenAIUrl,
				key: OpenAIKey
			});

			if (res) {
				OpenAIUrl = res.OPENAI_API_BASE_URL;
				OpenAIKey = res.OPENAI_API_KEY;
			}
88
89
90
		}
	};

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

Timothy J. Baek's avatar
Timothy J. Baek committed
94
		conversationMode = settings.conversationMode ?? false;
Timothy J. Baek's avatar
Timothy J. Baek committed
95
96
97
		speechAutoSend = settings.speechAutoSend ?? false;
		responseAutoPlayback = settings.responseAutoPlayback ?? false;

Timothy J. Baek's avatar
Timothy J. Baek committed
98
99
100
		STTEngine = settings?.audio?.STTEngine ?? '';
		TTSEngine = settings?.audio?.TTSEngine ?? '';
		speaker = settings?.audio?.speaker ?? '';
Timothy J. Baek's avatar
Timothy J. Baek committed
101

Timothy J. Baek's avatar
Timothy J. Baek committed
102
		if (TTSEngine === 'openai') {
Timothy J. Baek's avatar
Timothy J. Baek committed
103
104
105
106
			getOpenAIVoices();
		} else {
			getWebAPIVoices();
		}
107

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

Timothy J. Baek's avatar
Timothy J. Baek committed
111
112
113
114
			if (res) {
				OpenAIUrl = res.OPENAI_API_BASE_URL;
				OpenAIKey = res.OPENAI_API_KEY;
			}
115
		}
Timothy J. Baek's avatar
Timothy J. Baek committed
116
117
118
119
120
	});
</script>

<form
	class="flex flex-col h-full justify-between space-y-3 text-sm"
121
	on:submit|preventDefault={async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
122
123
124
		if ($user.role === 'admin') {
			await updateConfigHandler();
		}
Timothy J. Baek's avatar
Timothy J. Baek committed
125
		saveSettings({
Timothy J. Baek's avatar
Timothy J. Baek committed
126
			audio: {
Timothy J. Baek's avatar
Timothy J. Baek committed
127
128
				STTEngine: STTEngine !== '' ? STTEngine : undefined,
				TTSEngine: TTSEngine !== '' ? TTSEngine : undefined,
Timothy J. Baek's avatar
Timothy J. Baek committed
129
130
				speaker: speaker !== '' ? speaker : undefined
			}
Timothy J. Baek's avatar
Timothy J. Baek committed
131
132
133
134
		});
		dispatch('save');
	}}
>
135
	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[22rem]">
Timothy J. Baek's avatar
Timothy J. Baek committed
136
		<div>
137
			<div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
138
139

			<div class=" py-0.5 flex w-full justify-between">
140
				<div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
141
142
				<div class="flex items-center relative">
					<select
Jannik Streidl's avatar
Jannik Streidl committed
143
						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
144
						bind:value={STTEngine}
Timothy J. Baek's avatar
Timothy J. Baek committed
145
146
						placeholder="Select a mode"
						on:change={(e) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
147
148
							if (e.target.value !== '') {
								navigator.mediaDevices.getUserMedia({ audio: true }).catch(function (err) {
Ased Mammad's avatar
Ased Mammad committed
149
150
151
152
153
									toast.error(
										$i18n.t(`Permission denied when accessing microphone: {{error}}`, {
											error: err
										})
									);
Timothy J. Baek's avatar
Timothy J. Baek committed
154
155
									STTEngine = '';
								});
Timothy J. Baek's avatar
Timothy J. Baek committed
156
157
158
							}
						}}
					>
159
160
						<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
161
162
163
164
165
					</select>
				</div>
			</div>

			<div class=" py-0.5 flex w-full justify-between">
166
				<div class=" self-center text-xs font-medium">{$i18n.t('Conversation Mode')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
167
168
169
170
171
172
173
174
175

				<button
					class="p-1 px-3 text-xs flex rounded transition"
					on:click={() => {
						toggleConversationMode();
					}}
					type="button"
				>
					{#if conversationMode === true}
176
						<span class="ml-2 self-center">{$i18n.t('On')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
177
					{:else}
178
						<span class="ml-2 self-center">{$i18n.t('Off')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
179
180
181
182
183
					{/if}
				</button>
			</div>

			<div class=" py-0.5 flex w-full justify-between">
184
185
186
				<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
187
188
189
190
191
192
193
194
195

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

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

			<div class=" py-0.5 flex w-full justify-between">
208
				<div class=" self-center text-xs font-medium">{$i18n.t('Text-to-Speech Engine')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
209
210
				<div class="flex items-center relative">
					<select
Jannik Streidl's avatar
Jannik Streidl committed
211
						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
212
213
214
215
216
217
218
219
220
221
222
223
						bind:value={TTSEngine}
						placeholder="Select a mode"
						on:change={(e) => {
							if (e.target.value === 'openai') {
								getOpenAIVoices();
								speaker = 'alloy';
							} else {
								getWebAPIVoices();
								speaker = '';
							}
						}}
					>
224
225
						<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
226
227
228
					</select>
				</div>
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
229

Timothy J. Baek's avatar
Timothy J. Baek committed
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
			{#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}
248
249
			{/if}

Timothy J. Baek's avatar
Timothy J. Baek committed
250
			<div class=" py-0.5 flex w-full justify-between">
251
				<div class=" self-center text-xs font-medium">{$i18n.t('Auto-playback response')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
252
253
254
255
256

				<button
					class="p-1 px-3 text-xs flex rounded transition"
					on:click={() => {
						toggleResponseAutoPlayback();
Timothy J. Baek's avatar
Timothy J. Baek committed
257
					}}
Timothy J. Baek's avatar
Timothy J. Baek committed
258
					type="button"
Timothy J. Baek's avatar
Timothy J. Baek committed
259
				>
Timothy J. Baek's avatar
Timothy J. Baek committed
260
					{#if responseAutoPlayback === true}
261
						<span class="ml-2 self-center">{$i18n.t('On')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
262
					{:else}
263
						<span class="ml-2 self-center">{$i18n.t('Off')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
264
265
					{/if}
				</button>
Timothy J. Baek's avatar
Timothy J. Baek committed
266
267
268
269
270
			</div>
		</div>

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

Timothy J. Baek's avatar
Timothy J. Baek committed
271
		{#if TTSEngine === ''}
Timothy J. Baek's avatar
Timothy J. Baek committed
272
			<div>
273
				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
274
275
276
				<div class="flex w-full">
					<div class="flex-1">
						<select
Timothy J. Baek's avatar
Timothy J. Baek committed
277
							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
278
							bind:value={speaker}
Timothy J. Baek's avatar
Timothy J. Baek committed
279
280
							placeholder="Select a voice"
						>
Ased Mammad's avatar
Ased Mammad committed
281
							<option value="" selected>{$i18n.t('Default')}</option>
Timothy J. Baek's avatar
Timothy J. Baek committed
282
							{#each voices.filter((v) => v.localService === true) as voice}
Timothy J. Baek's avatar
Timothy J. Baek committed
283
284
285
286
287
288
289
								<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
290
		{:else if TTSEngine === 'openai'}
Timothy J. Baek's avatar
Timothy J. Baek committed
291
			<div>
292
				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
293
294
				<div class="flex w-full">
					<div class="flex-1">
295
296
297
						<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
298
299
							bind:value={speaker}
							placeholder="Select a voice"
300
301
302
						/>

						<datalist id="voice-list">
Timothy J. Baek's avatar
Timothy J. Baek committed
303
							{#each voices as voice}
304
								<option value={voice.name} />
Timothy J. Baek's avatar
Timothy J. Baek committed
305
							{/each}
306
						</datalist>
Timothy J. Baek's avatar
Timothy J. Baek committed
307
308
309
					</div>
				</div>
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
310
		{/if}
Timothy J. Baek's avatar
Timothy J. Baek committed
311
312
313
314
	</div>

	<div class="flex justify-end pt-3 text-sm font-medium">
		<button
Timothy J. Baek's avatar
Timothy J. Baek committed
315
			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
316
317
			type="submit"
		>
Jannik Streidl's avatar
Jannik Streidl committed
318
			{$i18n.t('Save')}
Timothy J. Baek's avatar
Timothy J. Baek committed
319
320
321
		</button>
	</div>
</form>