2023-06-11 19:18:58 +05:00
|
|
|
<script lang="ts">
|
2026-01-04 22:50:34 +05:00
|
|
|
import { onMount } from "svelte"
|
|
|
|
|
import { invoke } from "@tauri-apps/api/core"
|
|
|
|
|
import { goto } from "@roxi/routify"
|
|
|
|
|
import { setTimeout } from "worker-timers"
|
|
|
|
|
|
2026-01-06 23:32:58 +05:00
|
|
|
import { showInExplorer } from "@/functions"
|
2026-01-07 05:04:04 +05:00
|
|
|
import { appInfo, assistantVoice, translations, translate } from "@/stores"
|
2026-01-04 22:50:34 +05:00
|
|
|
|
|
|
|
|
import HDivider from "@/components/elements/HDivider.svelte"
|
|
|
|
|
import Footer from "@/components/Footer.svelte"
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
Notification,
|
|
|
|
|
Button,
|
|
|
|
|
Text,
|
|
|
|
|
Tabs,
|
|
|
|
|
Space,
|
|
|
|
|
Alert,
|
|
|
|
|
Input,
|
|
|
|
|
InputWrapper,
|
2026-01-06 23:32:58 +05:00
|
|
|
NativeSelect,
|
|
|
|
|
Switch
|
2026-01-04 22:50:34 +05:00
|
|
|
} from "@svelteuidev/core"
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
Check,
|
|
|
|
|
Mix,
|
|
|
|
|
Cube,
|
|
|
|
|
Code,
|
|
|
|
|
Gear,
|
|
|
|
|
QuestionMarkCircled,
|
|
|
|
|
CrossCircled
|
|
|
|
|
} from "radix-icons-svelte"
|
|
|
|
|
|
2026-01-07 05:04:04 +05:00
|
|
|
$: t = (key: string) => translate($translations, key)
|
|
|
|
|
|
2026-01-04 22:50:34 +05:00
|
|
|
// ### STATE
|
|
|
|
|
interface MicrophoneOption {
|
|
|
|
|
label: string
|
|
|
|
|
value: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let availableMicrophones: MicrophoneOption[] = []
|
2026-01-05 03:38:04 +05:00
|
|
|
let availableVoskModels: { label: string; value: string }[] = []
|
2026-01-04 22:50:34 +05:00
|
|
|
let settingsSaved = false
|
|
|
|
|
let saveButtonDisabled = false
|
|
|
|
|
|
2026-01-06 23:32:58 +05:00
|
|
|
// form values (state vars)
|
2026-01-04 22:50:34 +05:00
|
|
|
let voiceVal = ""
|
|
|
|
|
let selectedMicrophone = ""
|
|
|
|
|
let selectedWakeWordEngine = ""
|
2026-01-05 01:22:45 +05:00
|
|
|
let selectedIntentRecognitionEngine = ""
|
2026-01-05 03:38:04 +05:00
|
|
|
let selectedVoskModel = ""
|
2026-01-06 23:32:58 +05:00
|
|
|
let selectedNoiseSuppression = ""
|
|
|
|
|
let selectedVad = ""
|
|
|
|
|
let gainNormalizerEnabled = false
|
2026-01-04 22:50:34 +05:00
|
|
|
let apiKeyPicovoice = ""
|
|
|
|
|
let apiKeyOpenai = ""
|
|
|
|
|
|
|
|
|
|
// subscribe to stores
|
|
|
|
|
assistantVoice.subscribe(value => {
|
|
|
|
|
voiceVal = value
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
let feedbackLink = ""
|
|
|
|
|
let logFilePath = ""
|
|
|
|
|
appInfo.subscribe(info => {
|
|
|
|
|
feedbackLink = info.feedbackLink
|
|
|
|
|
logFilePath = info.logFilePath
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// ### FUNCTIONS
|
|
|
|
|
async function saveSettings() {
|
|
|
|
|
saveButtonDisabled = true
|
|
|
|
|
settingsSaved = false
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await Promise.all([
|
|
|
|
|
invoke("db_write", { key: "assistant_voice", val: voiceVal }),
|
|
|
|
|
invoke("db_write", { key: "selected_microphone", val: selectedMicrophone }),
|
|
|
|
|
invoke("db_write", { key: "selected_wake_word_engine", val: selectedWakeWordEngine }),
|
2026-01-05 01:22:45 +05:00
|
|
|
invoke("db_write", { key: "selected_intent_recognition_engine", val: selectedIntentRecognitionEngine }),
|
2026-01-05 03:38:04 +05:00
|
|
|
invoke("db_write", { key: "selected_vosk_model", val: selectedVoskModel }),
|
2026-01-06 23:32:58 +05:00
|
|
|
|
|
|
|
|
invoke("db_write", { key: "noise_suppression", val: selectedNoiseSuppression }),
|
|
|
|
|
invoke("db_write", { key: "vad", val: selectedVad }),
|
|
|
|
|
invoke("db_write", { key: "gain_normalizer", val: gainNormalizerEnabled.toString() }),
|
|
|
|
|
|
2026-01-04 22:50:34 +05:00
|
|
|
invoke("db_write", { key: "api_key__picovoice", val: apiKeyPicovoice }),
|
|
|
|
|
invoke("db_write", { key: "api_key__openai", val: apiKeyOpenai })
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
// update shared store
|
|
|
|
|
assistantVoice.set(voiceVal)
|
|
|
|
|
settingsSaved = true
|
|
|
|
|
|
|
|
|
|
// hide alert after 5 seconds
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
settingsSaved = false
|
|
|
|
|
}, 5000)
|
|
|
|
|
|
|
|
|
|
// restart listening with new settings
|
2026-01-06 23:32:58 +05:00
|
|
|
// stopListening(() => startListening())
|
2026-01-04 22:50:34 +05:00
|
|
|
} catch (err) {
|
|
|
|
|
console.error("failed to save settings:", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
saveButtonDisabled = false
|
|
|
|
|
}, 1000)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ### INIT
|
|
|
|
|
onMount(async () => {
|
|
|
|
|
try {
|
|
|
|
|
// load microphones
|
|
|
|
|
const mics = await invoke<string[]>("pv_get_audio_devices")
|
2026-01-06 23:32:58 +05:00
|
|
|
availableMicrophones = [
|
|
|
|
|
{ label: "По умолчанию (Система)", value: "-1" }, // system default
|
|
|
|
|
...mics.map((name, idx) => ({
|
|
|
|
|
label: name,
|
|
|
|
|
value: String(idx)
|
|
|
|
|
}))
|
|
|
|
|
]
|
2026-01-04 22:50:34 +05:00
|
|
|
|
2026-01-05 03:38:04 +05:00
|
|
|
// load vosk models
|
|
|
|
|
const voskModels = await invoke<{ name: string; language: string; size: string }[]>("list_vosk_models")
|
|
|
|
|
availableVoskModels = voskModels.map(m => ({
|
|
|
|
|
label: `${m.name} (${m.language}, ${m.size})`,
|
|
|
|
|
value: m.name
|
|
|
|
|
}))
|
|
|
|
|
|
2026-01-04 22:50:34 +05:00
|
|
|
// load settings from db
|
2026-01-06 23:32:58 +05:00
|
|
|
const [mic, wakeWord, intentReco, voskModel,
|
|
|
|
|
noiseSuppression, vad, gainNormalizer,
|
|
|
|
|
pico, openai] = await Promise.all([
|
2026-01-04 22:50:34 +05:00
|
|
|
invoke<string>("db_read", { key: "selected_microphone" }),
|
|
|
|
|
invoke<string>("db_read", { key: "selected_wake_word_engine" }),
|
2026-01-05 01:22:45 +05:00
|
|
|
invoke<string>("db_read", { key: "selected_intent_recognition_engine" }),
|
2026-01-05 03:38:04 +05:00
|
|
|
invoke<string>("db_read", { key: "selected_vosk_model" }),
|
2026-01-06 23:32:58 +05:00
|
|
|
|
|
|
|
|
invoke<string>("db_read", { key: "noise_suppression" }),
|
|
|
|
|
invoke<string>("db_read", { key: "vad" }),
|
|
|
|
|
invoke<string>("db_read", { key: "gain_normalizer" }),
|
|
|
|
|
|
2026-01-04 22:50:34 +05:00
|
|
|
invoke<string>("db_read", { key: "api_key__picovoice" }),
|
|
|
|
|
invoke<string>("db_read", { key: "api_key__openai" })
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
selectedMicrophone = mic
|
|
|
|
|
selectedWakeWordEngine = wakeWord
|
2026-01-05 01:22:45 +05:00
|
|
|
selectedIntentRecognitionEngine = intentReco
|
2026-01-05 03:38:04 +05:00
|
|
|
selectedVoskModel = voskModel
|
2026-01-06 23:32:58 +05:00
|
|
|
selectedNoiseSuppression = noiseSuppression
|
|
|
|
|
selectedVad = vad
|
|
|
|
|
gainNormalizerEnabled = gainNormalizer === "true"
|
2026-01-04 22:50:34 +05:00
|
|
|
apiKeyPicovoice = pico
|
|
|
|
|
apiKeyOpenai = openai
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("failed to load settings:", err)
|
|
|
|
|
}
|
|
|
|
|
})
|
2023-06-11 19:18:58 +05:00
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<Space h="xl" />
|
|
|
|
|
|
2026-01-04 22:50:34 +05:00
|
|
|
<Notification
|
2026-01-07 05:04:04 +05:00
|
|
|
title={t('settings-beta-title')}
|
2026-01-04 22:50:34 +05:00
|
|
|
icon={QuestionMarkCircled}
|
|
|
|
|
color="blue"
|
|
|
|
|
withCloseButton={false}
|
|
|
|
|
>
|
2026-01-07 05:04:04 +05:00
|
|
|
{t('settings-beta-desc')}<br />
|
|
|
|
|
{t('settings-beta-feedback')} <a href={feedbackLink} target="_blank">{t('settings-beta-bot')}</a>.
|
2026-01-04 22:50:34 +05:00
|
|
|
<Space h="sm" />
|
|
|
|
|
<Button
|
|
|
|
|
color="gray"
|
|
|
|
|
radius="md"
|
|
|
|
|
size="xs"
|
|
|
|
|
uppercase
|
|
|
|
|
on:click={() => showInExplorer(logFilePath)}
|
|
|
|
|
>
|
2026-01-07 05:04:04 +05:00
|
|
|
{t('settings-open-logs')}
|
2026-01-04 22:50:34 +05:00
|
|
|
</Button>
|
2023-06-11 19:18:58 +05:00
|
|
|
</Notification>
|
|
|
|
|
|
|
|
|
|
<Space h="xl" />
|
|
|
|
|
|
2026-01-04 22:50:34 +05:00
|
|
|
{#if settingsSaved}
|
|
|
|
|
<Notification
|
2026-01-07 05:04:04 +05:00
|
|
|
title={t('notification-saved')}
|
2026-01-04 22:50:34 +05:00
|
|
|
icon={Check}
|
|
|
|
|
color="teal"
|
|
|
|
|
on:close={() => { settingsSaved = false }}
|
2023-06-11 19:18:58 +05:00
|
|
|
/>
|
|
|
|
|
<Space h="xl" />
|
2026-01-04 22:50:34 +05:00
|
|
|
{/if}
|
2023-06-11 19:18:58 +05:00
|
|
|
|
2026-01-04 22:50:34 +05:00
|
|
|
<Tabs class="form" color="#8AC832" position="left">
|
2026-01-07 05:04:04 +05:00
|
|
|
<Tabs.Tab label={t('settings-general')} icon={Gear}>
|
2026-01-04 22:50:34 +05:00
|
|
|
<Space h="sm" />
|
|
|
|
|
<NativeSelect
|
|
|
|
|
data={[
|
2026-01-06 23:32:58 +05:00
|
|
|
{ label: "Jarvis New (ремастер)", value: "jarvis-remaster" },
|
|
|
|
|
{ label: "Рик из «Рик и Морти»", value: "rick-morty" },
|
|
|
|
|
{ label: "Jarvis (от Хауди)", value: "jarvis-howdy" },
|
2026-01-04 22:50:34 +05:00
|
|
|
{ label: "Jarvis OG (из фильмов)", value: "jarvis-og" }
|
|
|
|
|
]}
|
2026-01-07 05:04:04 +05:00
|
|
|
label={t('settings-voice')}
|
|
|
|
|
description={t('settings-voice-desc')}
|
2026-01-04 22:50:34 +05:00
|
|
|
variant="filled"
|
|
|
|
|
bind:value={voiceVal}
|
|
|
|
|
/>
|
|
|
|
|
</Tabs.Tab>
|
|
|
|
|
|
2026-01-07 05:04:04 +05:00
|
|
|
<Tabs.Tab label={t('settings-devices')} icon={Mix}>
|
2026-01-04 22:50:34 +05:00
|
|
|
<Space h="sm" />
|
|
|
|
|
<NativeSelect
|
|
|
|
|
data={availableMicrophones}
|
2026-01-07 05:04:04 +05:00
|
|
|
label={t('settings-microphone')}
|
|
|
|
|
description={t('settings-microphone-desc')}
|
2026-01-04 22:50:34 +05:00
|
|
|
variant="filled"
|
|
|
|
|
bind:value={selectedMicrophone}
|
|
|
|
|
/>
|
|
|
|
|
</Tabs.Tab>
|
|
|
|
|
|
2026-01-07 05:04:04 +05:00
|
|
|
<Tabs.Tab label={t('settings-neural-networks')} icon={Cube}>
|
2026-01-04 22:50:34 +05:00
|
|
|
<Space h="sm" />
|
|
|
|
|
<NativeSelect
|
|
|
|
|
data={[
|
|
|
|
|
{ label: "Rustpotter", value: "Rustpotter" },
|
2026-01-07 05:04:04 +05:00
|
|
|
{ label: "Vosk", value: "Vosk" },
|
|
|
|
|
{ label: "Picovoice Porcupine", value: "Picovoice" }
|
2026-01-04 22:50:34 +05:00
|
|
|
]}
|
2026-01-07 05:04:04 +05:00
|
|
|
label={t('settings-wake-word-engine')}
|
|
|
|
|
description={t('settings-wake-word-desc')}
|
2026-01-04 22:50:34 +05:00
|
|
|
variant="filled"
|
|
|
|
|
bind:value={selectedWakeWordEngine}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{#if selectedWakeWordEngine === "picovoice"}
|
|
|
|
|
<Space h="sm" />
|
2026-01-07 05:04:04 +05:00
|
|
|
<Alert title={t('settings-attention')} color="#868E96" variant="outline">
|
2026-01-04 22:50:34 +05:00
|
|
|
<Notification
|
2026-01-07 05:04:04 +05:00
|
|
|
title={t('settings-picovoice-warning')}
|
2026-01-04 22:50:34 +05:00
|
|
|
icon={CrossCircled}
|
|
|
|
|
color="orange"
|
|
|
|
|
withCloseButton={false}
|
|
|
|
|
>
|
2026-01-07 05:04:04 +05:00
|
|
|
{t('settings-picovoice-waiting')}
|
2026-01-04 22:50:34 +05:00
|
|
|
</Notification>
|
|
|
|
|
<Space h="sm" />
|
|
|
|
|
<Text size="sm" color="gray">
|
2026-01-07 05:04:04 +05:00
|
|
|
{t('settings-picovoice-key-desc')}
|
2026-01-04 22:50:34 +05:00
|
|
|
<a href="https://console.picovoice.ai/" target="_blank">Picovoice Console</a>.
|
|
|
|
|
</Text>
|
|
|
|
|
<Space h="sm" />
|
|
|
|
|
<Input
|
|
|
|
|
icon={Code}
|
2026-01-07 05:04:04 +05:00
|
|
|
placeholder={t('settings-picovoice-key')}
|
2026-01-04 22:50:34 +05:00
|
|
|
variant="filled"
|
|
|
|
|
autocomplete="off"
|
|
|
|
|
bind:value={apiKeyPicovoice}
|
|
|
|
|
/>
|
|
|
|
|
</Alert>
|
|
|
|
|
{/if}
|
|
|
|
|
|
2026-01-05 03:38:04 +05:00
|
|
|
<Space h="xl" />
|
|
|
|
|
{#key availableVoskModels}
|
|
|
|
|
<NativeSelect
|
|
|
|
|
data={[
|
2026-01-07 05:04:04 +05:00
|
|
|
{ label: t('settings-auto-detect'), value: "" },
|
2026-01-05 03:38:04 +05:00
|
|
|
...availableVoskModels
|
|
|
|
|
]}
|
2026-01-07 05:04:04 +05:00
|
|
|
label={t('settings-vosk-model')}
|
|
|
|
|
description={t('settings-vosk-model-desc')}
|
2026-01-05 03:38:04 +05:00
|
|
|
variant="filled"
|
|
|
|
|
bind:value={selectedVoskModel}
|
|
|
|
|
/>
|
|
|
|
|
{/key}
|
|
|
|
|
|
|
|
|
|
{#if availableVoskModels.length === 0}
|
|
|
|
|
<Space h="sm" />
|
2026-01-07 05:04:04 +05:00
|
|
|
<Alert title={t('settings-models-not-found')} color="orange" variant="outline">
|
2026-01-05 03:38:04 +05:00
|
|
|
<Text size="sm" color="gray">
|
2026-01-07 05:04:04 +05:00
|
|
|
{t('settings-models-hint')}
|
2026-01-05 03:38:04 +05:00
|
|
|
</Text>
|
|
|
|
|
</Alert>
|
|
|
|
|
{/if}
|
|
|
|
|
|
2026-01-04 22:50:34 +05:00
|
|
|
<Space h="xl" />
|
2026-01-05 01:22:45 +05:00
|
|
|
<NativeSelect
|
|
|
|
|
data={[
|
|
|
|
|
{ label: "Intent Classifier", value: "IntentClassifier" },
|
|
|
|
|
{ label: "Rasa", value: "Rasa" }
|
|
|
|
|
]}
|
2026-01-07 05:04:04 +05:00
|
|
|
label={t('settings-intent-engine')}
|
|
|
|
|
description={t('settings-intent-engine-desc')}
|
2026-01-05 01:22:45 +05:00
|
|
|
variant="filled"
|
|
|
|
|
bind:value={selectedIntentRecognitionEngine}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Space h="xl" />
|
2026-01-06 23:32:58 +05:00
|
|
|
|
|
|
|
|
<NativeSelect
|
|
|
|
|
data={[
|
2026-01-07 05:04:04 +05:00
|
|
|
{ label: t('settings-disabled'), value: "None" },
|
2026-01-06 23:32:58 +05:00
|
|
|
{ label: "Nnnoiseless", value: "Nnnoiseless" }
|
|
|
|
|
]}
|
2026-01-07 05:04:04 +05:00
|
|
|
label={t('settings-noise-suppression')}
|
|
|
|
|
description={t('settings-noise-suppression-desc')}
|
2026-01-06 23:32:58 +05:00
|
|
|
variant="filled"
|
|
|
|
|
bind:value={selectedNoiseSuppression}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Space h="md" />
|
|
|
|
|
|
|
|
|
|
<NativeSelect
|
|
|
|
|
data={[
|
2026-01-07 05:04:04 +05:00
|
|
|
{ label: t('settings-disabled'), value: "None" },
|
|
|
|
|
{ label: "Energy", value: "Energy" },
|
|
|
|
|
{ label: "Nnnoiseless", value: "Nnnoiseless" }
|
2026-01-06 23:32:58 +05:00
|
|
|
]}
|
2026-01-07 05:04:04 +05:00
|
|
|
label={t('settings-vad')}
|
|
|
|
|
description={t('settings-vad-desc')}
|
2026-01-06 23:32:58 +05:00
|
|
|
variant="filled"
|
|
|
|
|
bind:value={selectedVad}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Space h="md" />
|
|
|
|
|
|
2026-01-07 05:04:04 +05:00
|
|
|
<InputWrapper label={t('settings-gain-normalizer')}>
|
2026-01-06 23:32:58 +05:00
|
|
|
<Text size="sm" color="gray">
|
2026-01-07 05:04:04 +05:00
|
|
|
{t('settings-gain-normalizer-desc')}
|
2026-01-06 23:32:58 +05:00
|
|
|
</Text>
|
|
|
|
|
<Space h="xs" />
|
|
|
|
|
<Switch
|
2026-01-07 05:04:04 +05:00
|
|
|
label={gainNormalizerEnabled ? t('settings-enabled') : t('settings-disabled')}
|
2026-01-06 23:32:58 +05:00
|
|
|
bind:checked={gainNormalizerEnabled}
|
|
|
|
|
/>
|
|
|
|
|
</InputWrapper>
|
|
|
|
|
|
|
|
|
|
<Space h="xl" />
|
2026-01-04 22:50:34 +05:00
|
|
|
|
2026-01-07 05:04:04 +05:00
|
|
|
<InputWrapper label={t('settings-openai-key')}>
|
2026-01-04 22:50:34 +05:00
|
|
|
<Text size="sm" color="gray">
|
2026-01-07 05:04:04 +05:00
|
|
|
{t('settings-openai-not-supported')}
|
2026-01-04 22:50:34 +05:00
|
|
|
</Text>
|
|
|
|
|
<Space h="sm" />
|
|
|
|
|
<Input
|
|
|
|
|
icon={Code}
|
2026-01-07 05:04:04 +05:00
|
|
|
placeholder={t('settings-openai-key')}
|
2026-01-04 22:50:34 +05:00
|
|
|
variant="filled"
|
|
|
|
|
autocomplete="off"
|
|
|
|
|
bind:value={apiKeyOpenai}
|
|
|
|
|
disabled
|
|
|
|
|
/>
|
|
|
|
|
</InputWrapper>
|
|
|
|
|
</Tabs.Tab>
|
2023-06-11 19:18:58 +05:00
|
|
|
</Tabs>
|
|
|
|
|
|
|
|
|
|
<Space h="xl" />
|
|
|
|
|
|
2026-01-04 22:50:34 +05:00
|
|
|
<Button
|
|
|
|
|
color="lime"
|
|
|
|
|
radius="md"
|
|
|
|
|
size="sm"
|
|
|
|
|
uppercase
|
|
|
|
|
ripple
|
|
|
|
|
fullSize
|
|
|
|
|
on:click={saveSettings}
|
|
|
|
|
disabled={saveButtonDisabled}
|
|
|
|
|
>
|
2026-01-07 05:04:04 +05:00
|
|
|
{t('settings-save')}
|
2023-06-11 19:18:58 +05:00
|
|
|
</Button>
|
2026-01-04 22:50:34 +05:00
|
|
|
|
2023-06-11 19:18:58 +05:00
|
|
|
<Space h="sm" />
|
2026-01-04 22:50:34 +05:00
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
color="gray"
|
|
|
|
|
radius="md"
|
|
|
|
|
size="sm"
|
|
|
|
|
uppercase
|
|
|
|
|
fullSize
|
|
|
|
|
on:click={() => $goto("/")}
|
|
|
|
|
>
|
2026-01-07 05:04:04 +05:00
|
|
|
{t('settings-back')}
|
2023-06-11 19:18:58 +05:00
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
<HDivider />
|
2026-01-04 22:50:34 +05:00
|
|
|
<Footer />
|