| privatevoice.src/frontend/src/App.svelte | ●●●●● patch | view | raw | blame | history | |
| privatevoice.src/frontend/src/components/settings/LanguagePage.svelte | ●●●●● patch | view | raw | blame | history | |
| privatevoice.src/internal/model/model_test.go | ●●●●● patch | view | raw | blame | history | |
| privatevoice.src/internal/model/resolver.go | ●●●●● patch | view | raw | blame | history | |
| privatevoice.src/services/engine_service.go | ●●●●● patch | view | raw | blame | history |
privatevoice.src/frontend/src/App.svelte
@@ -16,6 +16,7 @@ let isIndicatorRoute = $state(window.location.hash === '#/indicator') let showUpdateDialog = $state(false) let languageReady = $state(false) let showOnboarding = $state(false) onMount(() => { Call.ByName('voicesnap/services.ConfigService.GetLanguageSettings').then((settings: any) => { @@ -53,6 +54,9 @@ const data = ev?.data if (data?.status) { engineStatus.set(data.status as any) if (data.status === 'ready') { showOnboarding = false } } if (data?.hardwareInfo) { engineHardwareInfo.set(data.hardwareInfo) @@ -79,6 +83,9 @@ Call.ByName('voicesnap/services.EngineService.GetStatus').then((data: any) => { if (data?.status) { engineStatus.set(data.status as any) if (data.status === 'ready') { showOnboarding = false } } if (data?.hardwareInfo) { engineHardwareInfo.set(data.hardwareInfo) @@ -112,13 +119,24 @@ try { const current: any = await Call.ByName('voicesnap/services.EngineService.GetCurrentModelStatus') if (!current?.installed) { const hasAnyInstalled: any = await Call.ByName('voicesnap/services.EngineService.HasAnyInstalledModel') showOnboarding = !hasAnyInstalled if (!hasAnyInstalled) { engineStatus.set('need_model') } } else { showOnboarding = false } } catch { try { const exists: any = await Call.ByName('voicesnap/services.EngineService.ModelExists') if (!exists) { const hasAnyInstalled: any = await Call.ByName('voicesnap/services.EngineService.HasAnyInstalledModel') .catch(() => false) showOnboarding = !hasAnyInstalled if (!hasAnyInstalled) { engineStatus.set('need_model') } } } catch {} } @@ -130,7 +148,7 @@ {:else} {#if languageReady} {#key $localeRevision} {#if $engineStatus === 'need_model'} {#if showOnboarding && $engineStatus === 'need_model'} <OnboardingView /> {:else} <SettingsWindow /> privatevoice.src/frontend/src/components/settings/LanguagePage.svelte
@@ -78,7 +78,6 @@ engineStatus.set('loading') } else { engineHardwareInfo.set('') engineStatus.set('need_model') } await Call.ByName('voicesnap/services.EngineService.ReloadCurrentModel') } catch {} @@ -125,13 +124,13 @@ } return t('settings.modelWorking') } if (option.isCurrent) return t('settings.modelCurrent') if (option.isCurrent && option.installed) return t('settings.modelCurrent') if (option.installed) return t('settings.modelUse') return t('settings.modelDownload') } async function onModelAction(option: ModelOption) { if (option.isCurrent || modelBusyID) return if ((option.isCurrent && option.installed) || modelBusyID) return modelBusyID = option.modelID modelProgress = 0 modelError = '' @@ -232,8 +231,8 @@ <div class="model-main"> <div class="model-title-row"> <span class="model-title">{option.displayName}</span> <span class="model-pill" class:current={option.isCurrent}> {option.isCurrent ? t('settings.modelCurrent') : modelTierLabel(option)} <span class="model-pill" class:current={option.isCurrent && option.installed}> {option.isCurrent && option.installed ? t('settings.modelCurrent') : modelTierLabel(option)} </span> </div> <span class="model-desc">{modelDescription(option)}</span> @@ -244,7 +243,7 @@ {/if} </span> </div> {#if option.isCurrent} {#if option.isCurrent && option.installed} <span class="model-current-status">{t('settings.modelCurrent')}</span> {:else} <button privatevoice.src/internal/model/model_test.go
@@ -592,6 +592,18 @@ } } func TestHasAnyUsableModelInRoot(t *testing.T) { root := t.TempDir() if HasAnyUsableModelInRoot(root) { t.Fatal("expected no usable model in empty root") } createValidSenseVoice(t, filepath.Join(root, "sensevoice-zh")) if !HasAnyUsableModelInRoot(root) { t.Fatal("expected installed sensevoice to count as a usable model") } } func TestInstallStateRoundTrip(t *testing.T) { root := t.TempDir() if err := UpdateInstalledModelState(root, DefaultModelProfile(), "sensevoice-zh", "new"); err != nil { privatevoice.src/internal/model/resolver.go
@@ -19,6 +19,20 @@ return ResolveModelInRoot(modelID, paths.ModelsRoot()) } func HasAnyUsableModel() bool { return HasAnyUsableModelInRoot(paths.ModelsRoot()) } func HasAnyUsableModelInRoot(modelsRoot string) bool { for _, profile := range ListModelProfiles() { resolved, err := ResolveModelInRoot(profile.ID, modelsRoot) if err == nil && resolved.IsUsable() { return true } } return false } func ResolveModelInRoot(modelID, modelsRoot string) (ResolvedModel, error) { profile, err := GetModelProfile(modelID) if err != nil { privatevoice.src/services/engine_service.go
@@ -36,6 +36,10 @@ return engine.ModelExists() } func (s *EngineService) HasAnyInstalledModel() bool { return model.HasAnyUsableModel() } func (s *EngineService) GetStatus() map[string]interface{} { s.mu.RLock() defer s.mu.RUnlock()