Ariver
2026-06-03 94992b563fdfddce683fc07ef5bb70e18ff910fe
privatevoice.src/services/engine_service.go
@@ -1,9 +1,13 @@
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"
@@ -11,6 +15,7 @@
// EngineService provides engine status and model management to the frontend.
type EngineService struct {
   cfg          *config.Config
   app          *application.App
   initCallback func()
   mu           sync.RWMutex
@@ -19,8 +24,11 @@
   lastError    string
}
func NewEngineService() *EngineService {
   return &EngineService{status: "loading"}
func NewEngineService(cfg *config.Config) *EngineService {
   if cfg == nil {
      cfg = config.Default()
   }
   return &EngineService{cfg: cfg, status: "loading"}
}
// ModelExists returns true if the ASR model files are present.
@@ -35,6 +43,117 @@
      "status":       s.status,
      "hardwareInfo": s.hardwareInfo,
      "error":        s.lastError,
   }
}
func (s *EngineService) GetCurrentModelStatus() map[string]interface{} {
   current := s.currentModel()
   return s.modelStatusMap(current.Profile, current)
}
func (s *EngineService) ListModelOptions() []map[string]interface{} {
   current := s.currentModel()
   languageProfile, err := model.GetLanguageProfile(current.LanguageSettings.EffectiveLanguageID)
   if err != nil {
      return nil
   }
   ids := make([]string, 0, 1+len(languageProfile.UpgradeModelIDs))
   ids = append(ids, languageProfile.DefaultModelID)
   ids = append(ids, languageProfile.UpgradeModelIDs...)
   options := make([]map[string]interface{}, 0, len(ids))
   for _, id := range ids {
      profile, err := model.GetModelProfile(id)
      if err != nil {
         continue
      }
      options = append(options, s.modelStatusMap(profile, current))
   }
   return options
}
func (s *EngineService) SelectModel(modelID string) error {
   profile, err := s.allowedModelProfile(modelID)
   if err != nil {
      return err
   }
   resolved, err := model.ResolveModel(profile.ID)
   if err != nil {
      return err
   }
   if !resolved.IsUsable() {
      return fmt.Errorf("model %s is not installed", profile.ID)
   }
   s.cfg.ModelSelectionMode = config.ModelSelectionModeManual
   s.cfg.SelectedModelID = profile.ID
   config.Save(s.cfg)
   s.ReloadCurrentModel()
   return nil
}
func (s *EngineService) DownloadModelByID(modelID string) error {
   profile, err := s.allowedModelProfile(modelID)
   if err != nil {
      return err
   }
   if len(profile.DownloadURLs) == 0 {
      return fmt.Errorf("no download URL configured for model %s", profile.ID)
   }
   err = model.DownloadProfile(profile, 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":    profile.ID,
            "modelName":  profile.DisplayName,
         })
      }
   })
   if err != nil {
      return err
   }
   s.cfg.ModelSelectionMode = config.ModelSelectionModeManual
   s.cfg.SelectedModelID = profile.ID
   config.Save(s.cfg)
   s.ReloadCurrentModel()
   return nil
}
func (s *EngineService) modelStatusMap(profile model.ModelProfile, current modelselection.CurrentModel) map[string]interface{} {
   resolved, err := model.ResolveModel(profile.ID)
   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":             profile.ID,
      "displayName":         profile.DisplayName,
      "backendKind":         profile.BackendKind,
      "tier":                profile.Tier,
      "languageID":          current.LanguageSettings.EffectiveLanguageID,
      "selectionMode":       current.SelectionMode,
      "isCurrent":           profile.ID == current.ModelID,
      "isDefault":           profile.ID == current.LanguageSettings.DefaultModelID,
      "installed":           installed,
      "installStatus":       status,
      "missing":             missing,
      "problems":            problems,
      "fallbackReason":      current.FallbackReason,
      "downloadSize":        profile.ApproxSize,
      "supportedLanguages":  profile.SupportedLanguageIDs,
      "recommendedLanguage": profile.RecommendedFor,
      "description":         profile.Description,
   }
}
@@ -67,6 +186,39 @@
   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
@@ -76,3 +228,36 @@
func (s *EngineService) SetApp(app *application.App) {
   s.app = app
}
func (s *EngineService) currentModel() modelselection.CurrentModel {
   if s.cfg == nil {
      cfg, err := config.Load()
      if err != nil {
         cfg = config.Default()
      }
      s.cfg = cfg
   }
   return modelselection.Resolve(s.cfg, language.NewSystemDetector())
}
func (s *EngineService) allowedModelProfile(modelID string) (model.ModelProfile, error) {
   profile, err := model.GetModelProfile(modelID)
   if err != nil {
      return model.ModelProfile{}, err
   }
   current := s.currentModel()
   languageProfile, err := model.GetLanguageProfile(current.LanguageSettings.EffectiveLanguageID)
   if err != nil {
      return model.ModelProfile{}, err
   }
   if profile.ID == languageProfile.DefaultModelID {
      return profile, nil
   }
   for _, id := range languageProfile.UpgradeModelIDs {
      if profile.ID == id {
         return profile, nil
      }
   }
   return model.ModelProfile{}, fmt.Errorf("model %s is not available for language %s", profile.ID, current.LanguageSettings.EffectiveLanguageID)
}