Audio.svelte 5.79 KB
Newer Older
Timothy J. Baek's avatar
Timothy J. Baek committed
1
<script lang="ts">
Timothy J. Baek's avatar
Timothy J. Baek committed
2
	import { user, settings, config } from '$lib/stores';
3
	import { createEventDispatcher, onMount, getContext } from 'svelte';
Jannik Streidl's avatar
Jannik Streidl committed
4
	import { toast } from 'svelte-sonner';
5
	import Switch from '$lib/components/common/Switch.svelte';
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
	let conversationMode = false;
Timothy J. Baek's avatar
Timothy J. Baek committed
14
15
	let speechAutoSend = false;
	let responseAutoPlayback = false;
16
	let nonLocalVoices = false;
Timothy J. Baek's avatar
Timothy J. Baek committed
17

Timothy J. Baek's avatar
Timothy J. Baek committed
18
	let STTEngine = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
19
20

	let voices = [];
Timothy J. Baek's avatar
Timothy J. Baek committed
21
	let voice = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
22

Timothy J. Baek's avatar
Timothy J. Baek committed
23
24
25
26
27
28
29
30
31
32
	const getOpenAIVoices = () => {
		voices = [
			{ name: 'alloy' },
			{ name: 'echo' },
			{ name: 'fable' },
			{ name: 'onyx' },
			{ name: 'nova' },
			{ name: 'shimmer' }
		];
	};
Timothy J. Baek's avatar
Timothy J. Baek committed
33

Timothy J. Baek's avatar
Timothy J. Baek committed
34
	const getWebAPIVoices = () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
35
		const getVoicesLoop = setInterval(async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
36
			voices = await speechSynthesis.getVoices();
Timothy J. Baek's avatar
Timothy J. Baek committed
37
38

			// do your loop
Timothy J. Baek's avatar
Timothy J. Baek committed
39
			if (voices.length > 0) {
Timothy J. Baek's avatar
Timothy J. Baek committed
40
41
42
				clearInterval(getVoicesLoop);
			}
		}, 100);
Timothy J. Baek's avatar
Timothy J. Baek committed
43
44
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
45
46
47
48
49
50
51
52
53
54
	const toggleResponseAutoPlayback = async () => {
		responseAutoPlayback = !responseAutoPlayback;
		saveSettings({ responseAutoPlayback: responseAutoPlayback });
	};

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

Timothy J. Baek's avatar
Timothy J. Baek committed
55
	onMount(async () => {
56
57
58
59
		conversationMode = $settings.conversationMode ?? false;
		speechAutoSend = $settings.speechAutoSend ?? false;
		responseAutoPlayback = $settings.responseAutoPlayback ?? false;

Timothy J. Baek's avatar
Timothy J. Baek committed
60
61
62
		STTEngine = $settings?.audio?.stt?.engine ?? '';
		voice = $settings?.audio?.tts?.voice ?? $config.audio.tts.voice ?? '';
		nonLocalVoices = $settings.audio?.tts?.nonLocalVoices ?? false;
Timothy J. Baek's avatar
Timothy J. Baek committed
63

Timothy J. Baek's avatar
Timothy J. Baek committed
64
		if ($config.audio.tts.engine === 'openai') {
Timothy J. Baek's avatar
Timothy J. Baek committed
65
66
67
68
			getOpenAIVoices();
		} else {
			getWebAPIVoices();
		}
Timothy J. Baek's avatar
Timothy J. Baek committed
69
70
71
72
73
	});
</script>

<form
	class="flex flex-col h-full justify-between space-y-3 text-sm"
74
	on:submit|preventDefault={async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
75
		saveSettings({
Timothy J. Baek's avatar
Timothy J. Baek committed
76
			audio: {
Timothy J. Baek's avatar
Timothy J. Baek committed
77
78
79
80
81
82
83
				stt: {
					engine: STTEngine !== '' ? STTEngine : undefined
				},
				tts: {
					voice: $config.audio.tts.engine === 'openai' ? voice : voice !== '' ? voice : undefined,
					nonLocalVoices: $config.audio.tts.engine === '' ? nonLocalVoices : undefined
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
84
			}
Timothy J. Baek's avatar
Timothy J. Baek committed
85
86
87
88
		});
		dispatch('save');
	}}
>
Timothy J. Baek's avatar
Timothy J. Baek committed
89
	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem]">
Timothy J. Baek's avatar
Timothy J. Baek committed
90
		<div>
91
			<div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
92

Timothy J. Baek's avatar
Timothy J. Baek committed
93
94
95
96
97
98
99
100
101
102
103
104
105
			{#if $config.audio.stt.engine !== 'web'}
				<div class=" py-0.5 flex w-full justify-between">
					<div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div>
					<div class="flex items-center relative">
						<select
							class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
							bind:value={STTEngine}
							placeholder="Select an engine"
						>
							<option value="">{$i18n.t('Default')}</option>
							<option value="web">{$i18n.t('Web API')}</option>
						</select>
					</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
106
				</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
107
			{/if}
Timothy J. Baek's avatar
Timothy J. Baek committed
108

Timothy J. Baek's avatar
Timothy J. Baek committed
109
			<div class=" py-0.5 flex w-full justify-between">
110
				<div class=" self-center text-xs font-medium">
111
					{$i18n.t('Instant Auto-Send After Voice Transcription')}
112
				</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
113
114
115
116
117
118
119
120
121

				<button
					class="p-1 px-3 text-xs flex rounded transition"
					on:click={() => {
						toggleSpeechAutoSend();
					}}
					type="button"
				>
					{#if speechAutoSend === true}
122
						<span class="ml-2 self-center">{$i18n.t('On')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
123
					{:else}
124
						<span class="ml-2 self-center">{$i18n.t('Off')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
125
126
127
					{/if}
				</button>
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
128
129
130
		</div>

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

Timothy J. Baek's avatar
Timothy J. Baek committed
133
			<div class=" py-0.5 flex w-full justify-between">
134
				<div class=" self-center text-xs font-medium">{$i18n.t('Auto-playback response')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
135
136
137
138
139

				<button
					class="p-1 px-3 text-xs flex rounded transition"
					on:click={() => {
						toggleResponseAutoPlayback();
Timothy J. Baek's avatar
Timothy J. Baek committed
140
					}}
Timothy J. Baek's avatar
Timothy J. Baek committed
141
					type="button"
Timothy J. Baek's avatar
Timothy J. Baek committed
142
				>
Timothy J. Baek's avatar
Timothy J. Baek committed
143
					{#if responseAutoPlayback === true}
144
						<span class="ml-2 self-center">{$i18n.t('On')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
145
					{:else}
146
						<span class="ml-2 self-center">{$i18n.t('Off')}</span>
Timothy J. Baek's avatar
Timothy J. Baek committed
147
148
					{/if}
				</button>
Timothy J. Baek's avatar
Timothy J. Baek committed
149
150
151
152
153
			</div>
		</div>

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

Timothy J. Baek's avatar
Timothy J. Baek committed
154
		{#if $config.audio.tts.engine === ''}
Timothy J. Baek's avatar
Timothy J. Baek committed
155
			<div>
156
				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
157
158
159
				<div class="flex w-full">
					<div class="flex-1">
						<select
Timothy J. Baek's avatar
Timothy J. Baek committed
160
							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
161
							bind:value={voice}
Timothy J. Baek's avatar
Timothy J. Baek committed
162
						>
Timothy J. Baek's avatar
Timothy J. Baek committed
163
164
							<option value="" selected={voice !== ''}>{$i18n.t('Default')}</option>
							{#each voices.filter((v) => nonLocalVoices || v.localService === true) as _voice}
165
								<option
Timothy J. Baek's avatar
Timothy J. Baek committed
166
									value={_voice.name}
167
									class="bg-gray-100 dark:bg-gray-700"
Timothy J. Baek's avatar
Timothy J. Baek committed
168
									selected={voice === _voice.name}>{_voice.name}</option
Timothy J. Baek's avatar
Timothy J. Baek committed
169
170
171
172
173
								>
							{/each}
						</select>
					</div>
				</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
174
175
				<div class="flex items-center justify-between my-1.5">
					<div class="text-xs">
176
177
178
179
180
181
182
						{$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
183
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
184
		{:else if $config.audio.tts.engine === 'openai'}
Timothy J. Baek's avatar
Timothy J. Baek committed
185
			<div>
186
				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
187
188
				<div class="flex w-full">
					<div class="flex-1">
189
190
191
						<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
192
							bind:value={voice}
Timothy J. Baek's avatar
Timothy J. Baek committed
193
							placeholder="Select a voice"
194
195
196
						/>

						<datalist id="voice-list">
Timothy J. Baek's avatar
Timothy J. Baek committed
197
							{#each voices as voice}
198
								<option value={voice.name} />
Timothy J. Baek's avatar
Timothy J. Baek committed
199
							{/each}
200
						</datalist>
Timothy J. Baek's avatar
Timothy J. Baek committed
201
202
203
					</div>
				</div>
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
204
		{/if}
Timothy J. Baek's avatar
Timothy J. Baek committed
205
206
	</div>

Timothy J. Baek's avatar
Timothy J. Baek committed
207
	<div class="flex justify-end text-sm font-medium">
Timothy J. Baek's avatar
Timothy J. Baek committed
208
		<button
Timothy J. Baek's avatar
Timothy J. Baek committed
209
			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
210
211
			type="submit"
		>
Jannik Streidl's avatar
Jannik Streidl committed
212
			{$i18n.t('Save')}
Timothy J. Baek's avatar
Timothy J. Baek committed
213
214
215
		</button>
	</div>
</form>