noise suppression added via nnnoiseless + vad + gain-normalizer + few frontend changes

This commit is contained in:
Priler 2026-01-06 23:32:58 +05:00
parent bd8e2c989d
commit 88ecf21b2c
73 changed files with 1235 additions and 112 deletions

View file

@ -1,8 +1,26 @@
<script lang="ts">
import { onMount, onDestroy } from "svelte"
import { Router } from "@roxi/routify"
import routes from "../.routify/routes.default.js"
import { SvelteUIProvider } from "@svelteuidev/core"
import Events from "./Events.svelte"
import {
loadVoiceSetting,
loadAppInfo,
startStatsPolling,
stopStatsPolling
} from "@/stores"
onMount(() => {
loadVoiceSetting()
loadAppInfo()
startStatsPolling(5000)
})
onDestroy(() => {
stopStatsPolling()
})
</script>
<SvelteUIProvider themeObserver="dark" withNormalizeCSS withGlobalStyles>

View file

@ -3,17 +3,24 @@
import { invoke } from "@tauri-apps/api/core"
import { capitalizeFirstLetter } from "@/functions"
import {
Text,
} from "@svelteuidev/core"
let jarvisStats = { running: false, ram_mb: 0, cpu_usage: 0 }
let microphoneLabel = ""
let wakeWordEngine = ""
let sttEngine = "Vosk"
let ramUsage = "-"
// let ramUsage = "-"
let interval: number | null = null
let statsUpdateInterval: number | null = null
async function updateRamUsage() {
async function updateStats() {
try {
const usage = await invoke<number>("get_current_ram_usage")
ramUsage = usage.toFixed(2)
jarvisStats = await invoke<{running: boolean, ram_mb: number, cpu_usage: number}>("get_jarvis_app_stats")
//const usage = await invoke<number>("get_current_ram_usage")
//ramUsage = usage.toFixed(2)
} catch (err) {
console.error("failed to get ram usage:", err)
}
@ -21,7 +28,8 @@
onMount(async () => {
// start polling ram usage
interval = setInterval(updateRamUsage, 1000) as unknown as number
updateStats()
statsUpdateInterval = setInterval(updateStats, 5000) as unknown as number
try {
// load microphone info
@ -37,8 +45,8 @@
})
onDestroy(() => {
if (interval) {
clearInterval(interval)
if (statsUpdateInterval) {
clearInterval(statsUpdateInterval)
}
})
</script>
@ -64,7 +72,12 @@
<div class="pulse"><div class="wave"></div></div>
<div class="info">
<span class="num">Ресурсы</span>
<small>RAM {ramUsage}mb</small>
{#if jarvisStats.running}
<small>RAM: {jarvisStats.ram_mb} MB</small>
<!--<Text>CPU: {jarvisStats.cpu_usage.toFixed(1)}%</Text>-->
{:else}
<Text color="gray">-</Text>
{/if}
</div>
</div>
</div>

View file

@ -11,17 +11,3 @@ export function showInExplorer(path: string): void {
invoke("show_in_folder", { path })
.catch(err => console.error("failed to open explorer:", err))
}
// ### LISTENER FUNCTIONS
// removed since gui now doesn't handle listening
export function startListening(): void {
// disabled in GUI - listening is handled by the tray app
console.log("[gui] listening not available in settings app")
}
export function stopListening(callback?: () => void): void {
// disabled in GUI - just call the callback if provided
console.log("[gui] listening not available in settings app")
if (callback) callback()
}

View file

@ -1,6 +1,7 @@
<script lang="ts">
import { onMount, onDestroy } from "svelte"
import { Notification, Space } from "@svelteuidev/core"
import { invoke } from "@tauri-apps/api/core"
import { Notification, Space, Button } from "@svelteuidev/core"
import { InfoCircled } from "radix-icons-svelte"
import SearchBar from "@/components/elements/SearchBar.svelte"
@ -9,11 +10,13 @@
import Stats from "@/components/elements/Stats.svelte"
import Footer from "@/components/Footer.svelte"
import { isListening } from "@/stores"
import { isJarvisRunning, updateJarvisStats } from "@/stores"
let listening = false
isListening.subscribe(value => {
listening = value
let running = false
let launching = false
isJarvisRunning.subscribe(value => {
running = value
})
onMount(() => {
@ -23,19 +26,48 @@
onDestroy(() => {
document.body.classList.remove("assist-page")
})
async function runAssistant() {
launching = true
try {
await invoke("run_jarvis_app")
// wait a bit then check if it's running
setTimeout(() => {
updateJarvisStats()
launching = false
}, 2000)
} catch (err) {
console.error("Failed to run jarvis-app:", err)
launching = false
}
}
</script>
<HDivider />
{#if !listening}
{#if !running}
<Notification
title="Внимание!"
icon={InfoCircled}
color="cyan"
withCloseButton={false}
>
В данный момент ассистент не прослушивает команды.<br />
Пожалуйста, <a href="/settings">перейдите в настройки</a> и введите ключ Picovoice.
В данный момент ассистент не запущен.<br />
Но вы всё еще можете изменять его настройки.<br />
<br />
<Button
color="lime"
radius="md"
size="sm"
uppercase
ripple
fullSize
on:click={runAssistant}
disabled={launching}
>
{launching ? "Запуск..." : "Запустить"}
</Button>
</Notification>
{:else}
<ArcReactor />
@ -43,4 +75,4 @@
<HDivider noMargin />
<Stats />
<Footer />
<Footer />

View file

@ -4,7 +4,7 @@
import { goto } from "@roxi/routify"
import { setTimeout } from "worker-timers"
import { showInExplorer, stopListening, startListening } from "@/functions"
import { showInExplorer } from "@/functions"
import { appInfo, assistantVoice } from "@/stores"
import HDivider from "@/components/elements/HDivider.svelte"
@ -19,7 +19,8 @@
Alert,
Input,
InputWrapper,
NativeSelect
NativeSelect,
Switch
} from "@svelteuidev/core"
import {
@ -43,12 +44,15 @@
let settingsSaved = false
let saveButtonDisabled = false
// form values
// form values (state vars)
let voiceVal = ""
let selectedMicrophone = ""
let selectedWakeWordEngine = ""
let selectedIntentRecognitionEngine = ""
let selectedVoskModel = ""
let selectedNoiseSuppression = ""
let selectedVad = ""
let gainNormalizerEnabled = false
let apiKeyPicovoice = ""
let apiKeyOpenai = ""
@ -76,6 +80,11 @@
invoke("db_write", { key: "selected_wake_word_engine", val: selectedWakeWordEngine }),
invoke("db_write", { key: "selected_intent_recognition_engine", val: selectedIntentRecognitionEngine }),
invoke("db_write", { key: "selected_vosk_model", val: selectedVoskModel }),
invoke("db_write", { key: "noise_suppression", val: selectedNoiseSuppression }),
invoke("db_write", { key: "vad", val: selectedVad }),
invoke("db_write", { key: "gain_normalizer", val: gainNormalizerEnabled.toString() }),
invoke("db_write", { key: "api_key__picovoice", val: apiKeyPicovoice }),
invoke("db_write", { key: "api_key__openai", val: apiKeyOpenai })
])
@ -90,7 +99,7 @@
}, 5000)
// restart listening with new settings
stopListening(() => startListening())
// stopListening(() => startListening())
} catch (err) {
console.error("failed to save settings:", err)
}
@ -105,10 +114,13 @@
try {
// load microphones
const mics = await invoke<string[]>("pv_get_audio_devices")
availableMicrophones = mics.map((name, idx) => ({
label: name,
value: String(idx)
}))
availableMicrophones = [
{ label: "По умолчанию (Система)", value: "-1" }, // system default
...mics.map((name, idx) => ({
label: name,
value: String(idx)
}))
]
// load vosk models
const voskModels = await invoke<{ name: string; language: string; size: string }[]>("list_vosk_models")
@ -118,11 +130,18 @@
}))
// load settings from db
const [mic, wakeWord, intentReco, voskModel, pico, openai] = await Promise.all([
const [mic, wakeWord, intentReco, voskModel,
noiseSuppression, vad, gainNormalizer,
pico, openai] = await Promise.all([
invoke<string>("db_read", { key: "selected_microphone" }),
invoke<string>("db_read", { key: "selected_wake_word_engine" }),
invoke<string>("db_read", { key: "selected_intent_recognition_engine" }),
invoke<string>("db_read", { key: "selected_vosk_model" }),
invoke<string>("db_read", { key: "noise_suppression" }),
invoke<string>("db_read", { key: "vad" }),
invoke<string>("db_read", { key: "gain_normalizer" }),
invoke<string>("db_read", { key: "api_key__picovoice" }),
invoke<string>("db_read", { key: "api_key__openai" })
])
@ -131,6 +150,9 @@
selectedWakeWordEngine = wakeWord
selectedIntentRecognitionEngine = intentReco
selectedVoskModel = voskModel
selectedNoiseSuppression = noiseSuppression
selectedVad = vad
gainNormalizerEnabled = gainNormalizer === "true"
apiKeyPicovoice = pico
apiKeyOpenai = openai
} catch (err) {
@ -179,7 +201,9 @@
<Space h="sm" />
<NativeSelect
data={[
{ label: "Jarvis ремейк (от Хауди)", value: "jarvis-remake" },
{ label: "Jarvis New (ремастер)", value: "jarvis-remaster" },
{ label: "Рик из «Рик и Морти»", value: "rick-morty" },
{ label: "Jarvis (от Хауди)", value: "jarvis-howdy" },
{ label: "Jarvis OG (из фильмов)", value: "jarvis-og" }
]}
label="Голос ассистента"
@ -281,6 +305,46 @@
<Space h="xl" />
<NativeSelect
data={[
{ label: "Отключено", value: "None" },
{ label: "Nnnoiseless", value: "Nnnoiseless" }
]}
label="Шумоподавление"
description="Уменьшает фоновый шум. Может ухудшить распознавание в некоторых случаях."
variant="filled"
bind:value={selectedNoiseSuppression}
/>
<Space h="md" />
<NativeSelect
data={[
{ label: "Отключено", value: "None" },
{ label: "Energy (простой)", value: "Energy" },
{ label: "Nnnoiseless (нейросеть)", value: "Nnnoiseless" }
]}
label="Определение голосой активности (VAD)"
description="Пропускает тишину, экономит ресурсы CPU."
variant="filled"
bind:value={selectedVad}
/>
<Space h="md" />
<InputWrapper label="Нормализация громкости">
<Text size="sm" color="gray">
Автоматически регулирует уровень громкости.
</Text>
<Space h="xs" />
<Switch
label={gainNormalizerEnabled ? "Включено" : "Выключено"}
bind:checked={gainNormalizerEnabled}
/>
</InputWrapper>
<Space h="xl" />
<InputWrapper label="Ключ OpenAI">
<Text size="sm" color="gray">
В данный момент ChatGPT <u>не поддерживается</u>.

View file

@ -1,26 +1,15 @@
import { writable, get } from "svelte/store"
import { writable } from "svelte/store"
import { invoke } from "@tauri-apps/api/core"
// ### LISTENING STATE
// note: defaults to false since GUI doesn't have listening capability
export const isListening = writable(false)
// ### RUNNING STATE
export const isJarvisRunning = writable(false)
export const jarvisRamUsage = writable(0)
export const jarvisCpuUsage = writable(0)
// ### ASSISTANT VOICE
export const assistantVoice = writable("")
// load voice setting from db
async function loadVoiceSetting() {
try {
const voice = await invoke<string>("db_read", { key: "assistant_voice" })
assistantVoice.set(voice)
} catch (err) {
console.error("failed to load voice setting:", err)
}
}
loadVoiceSetting()
// ### APP INFO
// these are loaded once on startup
export const appInfo = writable({
tgOfficialLink: "",
feedbackLink: "",
@ -28,7 +17,17 @@ export const appInfo = writable({
logFilePath: ""
})
async function loadAppInfo() {
// ### INIT FUNCTIONS (call these from a component)
export async function loadVoiceSetting() {
try {
const voice = await invoke<string>("db_read", { key: "assistant_voice" })
assistantVoice.set(voice)
} catch (err) {
console.error("failed to load voice setting:", err)
}
}
export async function loadAppInfo() {
try {
const [tg, feedback, repo, logPath] = await Promise.all([
invoke<string>("get_tg_official_link"),
@ -47,4 +46,31 @@ async function loadAppInfo() {
console.error("failed to load app info:", err)
}
}
loadAppInfo()
export async function updateJarvisStats() {
try {
const stats = await invoke<{running: boolean, ram_mb: number, cpu_usage: number}>("get_jarvis_app_stats")
isJarvisRunning.set(stats.running)
jarvisRamUsage.set(stats.ram_mb)
jarvisCpuUsage.set(stats.cpu_usage)
} catch (err) {
console.error("failed to get jarvis stats:", err)
}
}
// polling manager
let statsInterval: ReturnType<typeof setInterval> | null = null
export function startStatsPolling(intervalMs = 5000) {
if (statsInterval) return // already running
updateJarvisStats()
statsInterval = setInterval(updateJarvisStats, intervalMs)
}
export function stopStatsPolling() {
if (statsInterval) {
clearInterval(statsInterval)
statsInterval = null
}
}