Audio.svelte 9.48 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
refac  
Timothy J. Baek committed
171
						placeholder="Select an engine"
Timothy J. Baek's avatar
Timothy J. Baek committed
172
						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
							}
						}}
					>
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
185
186
						<option value="">{$i18n.t('Default (Whisper)')}</option>
						<option value="web">{$i18n.t('Web API')}</option>
Timothy J. Baek's avatar
Timothy J. Baek committed
187
188
189
190
					</select>
				</div>
			</div>

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

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

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

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

Timothy J. Baek's avatar
Timothy J. Baek committed
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
			{#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}
257
258
			{/if}

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

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

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

Timothy J. Baek's avatar
Timothy J. Baek committed
280
		{#if TTSEngine === ''}
Timothy J. Baek's avatar
Timothy J. Baek committed
281
			<div>
282
				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
283
284
285
				<div class="flex w-full">
					<div class="flex-1">
						<select
Timothy J. Baek's avatar
Timothy J. Baek committed
286
							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
287
							bind:value={speaker}
Timothy J. Baek's avatar
Timothy J. Baek committed
288
						>
289
290
291
292
293
294
							<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
295
296
297
298
299
								>
							{/each}
						</select>
					</div>
				</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
300
301
				<div class="flex items-center justify-between my-1.5">
					<div class="text-xs">
302
303
304
305
306
307
308
						{$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
309
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
310
		{:else if TTSEngine === 'openai'}
Timothy J. Baek's avatar
Timothy J. Baek committed
311
			<div>
312
				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
313
314
				<div class="flex w-full">
					<div class="flex-1">
315
316
317
						<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"
318
							bind:value={OpenAISpeaker}
Timothy J. Baek's avatar
Timothy J. Baek committed
319
							placeholder="Select a voice"
320
321
322
						/>

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

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

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