Sync model download state in settings
2 files modified
1 files added
| | |
| | | |
| | | Events.On('model:download-progress', (ev: any) => { |
| | | const data = ev?.data |
| | | if (data?.modelID && data.modelID === modelBusyID && typeof data.percent === 'number') { |
| | | if (data?.modelID && typeof data.percent === 'number') { |
| | | modelBusyID = data.modelID |
| | | modelBusyKind = 'download' |
| | | if (modelCancellingID !== data.modelID) { |
| | | modelCancellingID = '' |
| | | } |
| | | modelProgress = data.percent |
| | | } |
| | | }) |
| | | |
| | | Events.On('model:download-cancelled', (ev: any) => { |
| | | const data = ev?.data |
| | | if (data?.modelID) { |
| | | modelBusyID = data.modelID |
| | | modelBusyKind = 'download' |
| | | modelCancellingID = data.modelID |
| | | } |
| | | }) |
| | | |
| | | Events.On('model:download-finished', (ev: any) => { |
| | | const data = ev?.data |
| | | if (!data?.modelID || data.modelID === modelBusyID) { |
| | | clearModelBusy() |
| | | loadModelOptions() |
| | | } |
| | | }) |
| | | |
| | |
| | | applyLanguageSettings(lang) |
| | | } catch {} |
| | | await loadModelOptions() |
| | | await syncModelDownloadStatus() |
| | | settingsLoaded = true |
| | | } |
| | | loadSettings() |
| | |
| | | } catch { |
| | | modelOptions = [] |
| | | } |
| | | } |
| | | |
| | | async function syncModelDownloadStatus(): Promise<boolean> { |
| | | try { |
| | | const status: any = await Call.ByName('voicesnap/services.EngineService.GetModelDownloadStatus') |
| | | return applyModelDownloadStatus(status) |
| | | } catch { |
| | | return false |
| | | } |
| | | } |
| | | |
| | | function applyModelDownloadStatus(status: any): boolean { |
| | | if (status?.active && status?.modelID) { |
| | | modelBusyID = status.modelID |
| | | modelBusyKind = 'download' |
| | | modelCancellingID = status.cancelling ? status.modelID : '' |
| | | modelProgress = typeof status.percent === 'number' ? status.percent : 0 |
| | | modelError = '' |
| | | return true |
| | | } |
| | | if (modelBusyKind === 'download') { |
| | | clearModelBusy() |
| | | } |
| | | return false |
| | | } |
| | | |
| | | function clearModelBusy() { |
| | | modelBusyID = '' |
| | | modelBusyKind = '' |
| | | modelCancellingID = '' |
| | | modelProgress = 0 |
| | | } |
| | | |
| | | function modelDescription(option: ModelOption): string { |
| | |
| | | } |
| | | |
| | | function isCancelError(err: any): boolean { |
| | | const text = String(err?.message || err || '').toLowerCase() |
| | | const text = errorMessage(err).toLowerCase() |
| | | return text.includes('context canceled') || text.includes('cancelled') || text.includes('canceled') |
| | | } |
| | | |
| | | function isAlreadyDownloadingError(err: any): boolean { |
| | | return errorMessage(err).toLowerCase().includes('already downloading') |
| | | } |
| | | |
| | | function errorMessage(err: any): string { |
| | | const raw = String(err?.message || err || '') |
| | | if (!raw.startsWith('{')) return raw |
| | | try { |
| | | const parsed = JSON.parse(raw) |
| | | return String(parsed?.message || raw) |
| | | } catch { |
| | | return raw |
| | | } |
| | | } |
| | | |
| | | function canCancelModel(option: ModelOption): boolean { |
| | |
| | | try { |
| | | const cancelled: any = await Call.ByName('voicesnap/services.EngineService.CancelModelDownload', option.modelID) |
| | | if (!cancelled) { |
| | | modelBusyID = '' |
| | | modelBusyKind = '' |
| | | modelCancellingID = '' |
| | | modelProgress = 0 |
| | | clearModelBusy() |
| | | await loadModelOptions() |
| | | await syncModelDownloadStatus() |
| | | } |
| | | } catch (err: any) { |
| | | modelError = err?.message || String(err || t('settings.modelActionFailed')) |
| | | modelBusyID = '' |
| | | modelBusyKind = '' |
| | | modelCancellingID = '' |
| | | modelProgress = 0 |
| | | modelError = errorMessage(err) || t('settings.modelActionFailed') |
| | | clearModelBusy() |
| | | await loadModelOptions() |
| | | await syncModelDownloadStatus() |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | await loadModelOptions() |
| | | } catch (err: any) { |
| | | if (!isCancelError(err)) { |
| | | modelError = err?.message || String(err || t('settings.modelActionFailed')) |
| | | if (isAlreadyDownloadingError(err)) { |
| | | const active = await syncModelDownloadStatus() |
| | | if (!active) { |
| | | modelError = errorMessage(err) |
| | | } |
| | | } else if (!isCancelError(err)) { |
| | | modelError = errorMessage(err) || t('settings.modelActionFailed') |
| | | } |
| | | await loadModelOptions() |
| | | } finally { |
| | | if (modelBusyID === option.modelID) { |
| | | modelBusyID = '' |
| | | modelBusyKind = '' |
| | | modelCancellingID = '' |
| | | modelProgress = 0 |
| | | const active = await syncModelDownloadStatus() |
| | | if (!active || modelBusyID !== option.modelID) { |
| | | clearModelBusy() |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | applyLanguageSettings(settings) |
| | | await syncEngineForCurrentModel() |
| | | await loadModelOptions() |
| | | await syncModelDownloadStatus() |
| | | } catch { |
| | | languageModeVal = prevMode |
| | | languageIDVal = prevID |
| | |
| | | downloadMu sync.Mutex |
| | | downloadID string |
| | | downloadCancel context.CancelFunc |
| | | downloadCancelling bool |
| | | downloadProgress float64 |
| | | downloadDownloaded int64 |
| | | downloadTotal int64 |
| | | } |
| | | |
| | | func NewEngineService(cfg *config.Config) *EngineService { |
| | |
| | | defer finish() |
| | | |
| | | err = model.DownloadProfileWithContext(ctx, profile, profile.DownloadURLs, paths.ModelsRoot(), func(percent float64, downloaded, total int64) { |
| | | s.updateModelDownloadProgress(profile.ID, percent, downloaded, total) |
| | | if s.app != nil { |
| | | s.app.Event.Emit("model:download-progress", map[string]interface{}{ |
| | | "percent": percent, |
| | |
| | | s.downloadMu.Unlock() |
| | | return false |
| | | } |
| | | s.downloadCancelling = true |
| | | s.downloadCancel() |
| | | s.downloadMu.Unlock() |
| | | |
| | |
| | | }) |
| | | } |
| | | return true |
| | | } |
| | | |
| | | func (s *EngineService) GetModelDownloadStatus() map[string]interface{} { |
| | | s.downloadMu.Lock() |
| | | defer s.downloadMu.Unlock() |
| | | return s.modelDownloadStatusLocked() |
| | | } |
| | | |
| | | func (s *EngineService) modelStatusMap(profile model.ModelProfile, current modelselection.CurrentModel) map[string]interface{} { |
| | |
| | | defer finish() |
| | | |
| | | err = model.DownloadProfileWithContext(ctx, profile, []string{primaryURL, fallbackURL}, paths.ModelsRoot(), func(percent float64, downloaded, total int64) { |
| | | s.updateModelDownloadProgress(profile.ID, percent, downloaded, total) |
| | | if s.app != nil { |
| | | s.app.Event.Emit("model:download-progress", map[string]interface{}{ |
| | | "percent": percent, |
| | |
| | | defer finish() |
| | | |
| | | err = model.DownloadProfileWithContext(ctx, current.Profile, current.Profile.DownloadURLs, paths.ModelsRoot(), func(percent float64, downloaded, total int64) { |
| | | s.updateModelDownloadProgress(current.ModelID, percent, downloaded, total) |
| | | if s.app != nil { |
| | | s.app.Event.Emit("model:download-progress", map[string]interface{}{ |
| | | "percent": percent, |
| | |
| | | ctx, cancel := context.WithCancel(context.Background()) |
| | | s.downloadID = modelID |
| | | s.downloadCancel = cancel |
| | | s.downloadCancelling = false |
| | | s.downloadProgress = 0 |
| | | s.downloadDownloaded = 0 |
| | | s.downloadTotal = 0 |
| | | finish := func() { |
| | | emitFinished := false |
| | | s.downloadMu.Lock() |
| | | if s.downloadID == modelID { |
| | | s.downloadID = "" |
| | | s.downloadCancel = nil |
| | | s.downloadCancelling = false |
| | | s.downloadProgress = 0 |
| | | s.downloadDownloaded = 0 |
| | | s.downloadTotal = 0 |
| | | emitFinished = true |
| | | } |
| | | s.downloadMu.Unlock() |
| | | if emitFinished && s.app != nil { |
| | | s.app.Event.Emit("model:download-finished", map[string]interface{}{ |
| | | "modelID": modelID, |
| | | }) |
| | | } |
| | | } |
| | | return ctx, finish, nil |
| | | } |
| | | |
| | | func (s *EngineService) updateModelDownloadProgress(modelID string, percent float64, downloaded, total int64) { |
| | | s.downloadMu.Lock() |
| | | if s.downloadID == modelID && s.downloadCancel != nil { |
| | | s.downloadProgress = percent |
| | | s.downloadDownloaded = downloaded |
| | | s.downloadTotal = total |
| | | } |
| | | s.downloadMu.Unlock() |
| | | } |
| | | return ctx, finish, nil |
| | | |
| | | func (s *EngineService) modelDownloadStatusLocked() map[string]interface{} { |
| | | return map[string]interface{}{ |
| | | "active": s.downloadCancel != nil, |
| | | "modelID": s.downloadID, |
| | | "cancelling": s.downloadCancelling, |
| | | "percent": s.downloadProgress, |
| | | "downloaded": s.downloadDownloaded, |
| | | "total": s.downloadTotal, |
| | | } |
| | | } |
| | | |
| | | func (s *EngineService) allowedModelProfile(modelID string) (model.ModelProfile, error) { |
| New file |
| | |
| | | package services |
| | | |
| | | import "testing" |
| | | |
| | | func TestEngineServiceDownloadStatusTracksCancelAndFinish(t *testing.T) { |
| | | service := NewEngineService(nil) |
| | | status := service.GetModelDownloadStatus() |
| | | if status["active"].(bool) { |
| | | t.Fatal("new service should not report an active model download") |
| | | } |
| | | |
| | | _, finish, err := service.beginModelDownload("zipformer-ko") |
| | | if err != nil { |
| | | t.Fatal(err) |
| | | } |
| | | status = service.GetModelDownloadStatus() |
| | | if !status["active"].(bool) { |
| | | t.Fatal("active download was not reported") |
| | | } |
| | | if got := status["modelID"]; got != "zipformer-ko" { |
| | | t.Fatalf("modelID = %v, want zipformer-ko", got) |
| | | } |
| | | if status["cancelling"].(bool) { |
| | | t.Fatal("new download should not start in cancelling state") |
| | | } |
| | | |
| | | service.updateModelDownloadProgress("zipformer-ko", 42.5, 425, 1000) |
| | | status = service.GetModelDownloadStatus() |
| | | if got := status["percent"]; got != 42.5 { |
| | | t.Fatalf("percent = %v, want 42.5", got) |
| | | } |
| | | if got := status["downloaded"]; got != int64(425) { |
| | | t.Fatalf("downloaded = %v, want 425", got) |
| | | } |
| | | if got := status["total"]; got != int64(1000) { |
| | | t.Fatalf("total = %v, want 1000", got) |
| | | } |
| | | |
| | | if !service.CancelModelDownload("zipformer-ko") { |
| | | t.Fatal("expected active download to be cancelled") |
| | | } |
| | | status = service.GetModelDownloadStatus() |
| | | if !status["cancelling"].(bool) { |
| | | t.Fatal("cancelled download should report cancelling state until finish") |
| | | } |
| | | |
| | | finish() |
| | | status = service.GetModelDownloadStatus() |
| | | if status["active"].(bool) { |
| | | t.Fatal("finished download should clear active state") |
| | | } |
| | | if got := status["modelID"]; got != "" { |
| | | t.Fatalf("modelID after finish = %v, want empty", got) |
| | | } |
| | | } |