package services import ( "fmt" "sync" "voicesnap/internal/config" "voicesnap/internal/engine" "voicesnap/internal/language" "voicesnap/internal/model" "voicesnap/internal/modelselection" "voicesnap/internal/paths" "github.com/wailsapp/wails/v3/pkg/application" ) // EngineService provides engine status and model management to the frontend. type EngineService struct { app *application.App initCallback func() mu sync.RWMutex status string hardwareInfo string lastError string } func NewEngineService() *EngineService { return &EngineService{status: "loading"} } // ModelExists returns true if the ASR model files are present. func (s *EngineService) ModelExists() bool { return engine.ModelExists() } func (s *EngineService) GetStatus() map[string]interface{} { s.mu.RLock() defer s.mu.RUnlock() return map[string]interface{}{ "status": s.status, "hardwareInfo": s.hardwareInfo, "error": s.lastError, } } func (s *EngineService) GetCurrentModelStatus() map[string]interface{} { current := s.currentModel() resolved, err := model.ResolveModel(current.ModelID) installed := err == nil && resolved.IsUsable() status := model.ModelNotInstalled var missing []string var problems []string if err == nil { status = resolved.Status missing = resolved.Missing problems = resolved.Problems } return map[string]interface{}{ "modelID": current.ModelID, "displayName": current.Profile.DisplayName, "backendKind": current.Profile.BackendKind, "languageID": current.LanguageSettings.EffectiveLanguageID, "selectionMode": current.SelectionMode, "installed": installed, "installStatus": status, "missing": missing, "problems": problems, "fallbackReason": current.FallbackReason, "downloadSize": current.Profile.ApproxSize, "supportedLanguages": current.Profile.SupportedLanguageIDs, "recommendedLanguage": current.Profile.RecommendedFor, } } func (s *EngineService) SetStatus(status, hardwareInfo, lastError string) { s.mu.Lock() defer s.mu.Unlock() s.status = status s.hardwareInfo = hardwareInfo s.lastError = lastError } // DownloadModel downloads the ASR model with progress events. func (s *EngineService) DownloadModel(primaryURL, fallbackURL string) error { err := model.Download(primaryURL, fallbackURL, paths.ModelsRoot(), func(percent float64, downloaded, total int64) { if s.app != nil { s.app.Event.Emit("model:download-progress", map[string]interface{}{ "percent": percent, "downloaded": downloaded, "total": total, }) } }) if err != nil { return err } if s.initCallback != nil { go s.initCallback() } return nil } func (s *EngineService) DownloadCurrentModel() error { current := s.currentModel() if len(current.Profile.DownloadURLs) == 0 { return fmt.Errorf("no download URL configured for model %s", current.ModelID) } err := model.DownloadProfile(current.Profile, current.Profile.DownloadURLs, paths.ModelsRoot(), func(percent float64, downloaded, total int64) { if s.app != nil { s.app.Event.Emit("model:download-progress", map[string]interface{}{ "percent": percent, "downloaded": downloaded, "total": total, "modelID": current.ModelID, "modelName": current.Profile.DisplayName, }) } }) if err != nil { return err } if s.initCallback != nil { go s.initCallback() } return nil } func (s *EngineService) ReloadCurrentModel() { if s.initCallback != nil { go s.initCallback() } } // SetInitCallback sets the callback to re-initialize the engine after model download. func (s *EngineService) SetInitCallback(cb func()) { s.initCallback = cb } // SetApp sets the Wails app reference for event emission. func (s *EngineService) SetApp(app *application.App) { s.app = app } func (s *EngineService) currentModel() modelselection.CurrentModel { cfg, err := config.Load() if err != nil { cfg = config.Default() } return modelselection.Resolve(cfg, language.NewSystemDetector()) }