Cw
2026-07-04 fecea2c7a4ecc48b2354868894e78be361a2ccf1
C1.source/privatevoice.src/app.go
@@ -3,6 +3,7 @@
import (
   "context"
   "fmt"
   "runtime"
   "strings"
   "sync"
   "sync/atomic"
@@ -17,6 +18,8 @@
   "voicesnap/internal/input"
   "voicesnap/internal/language"
   "voicesnap/internal/logger"
   "voicesnap/internal/model"
   "voicesnap/internal/modelselection"
   "voicesnap/internal/overlay"
   "voicesnap/internal/sound"
   "voicesnap/internal/textproc"
@@ -28,8 +31,8 @@
)
const (
   appVersion        = "2.2.0"
   appBuild          = "20260702.0303"
   appVersion        = "2.2.2"
   appBuild          = "20260703.0335"
   appDisplayVersion = appVersion + " (build " + appBuild + ")"
   appName           = "PrivateVoice Dictation"
@@ -50,14 +53,18 @@
   cancel context.CancelFunc
   wg     sync.WaitGroup
   cfg      *config.Config
   recorder *audio.Recorder
   eng      engine.Engine
   engineMu sync.Mutex
   hk       hotkey.Listener
   paster   input.Paster
   history  *history.Store
   userdict *userdict.Store
   cfg           *config.Config
   recorder      *audio.Recorder
   eng           engine.Engine
   engineMu      sync.Mutex
   engineInitMu  sync.Mutex
   engineInitSeq atomic.Uint64
   engineMeta    engineMetadata
   engineFactory engineFactoryFunc
   hk            hotkey.Listener
   paster        input.Paster
   history       *history.Store
   userdict      *userdict.Store
   engineService  *services.EngineService
   wailsApp       *application.App
@@ -86,6 +93,24 @@
   liveCaptionSeq          uint64
   releaseTailCaptureNanos atomic.Int64
   holdPreCaptureEnabled   atomic.Bool
}
type engineFactoryFunc func(initID uint64) (engine.Engine, engineMetadata, bool, error)
type engineMetadata struct {
   InitID                uint64
   ResolvedModelID       string
   ConfigSelectedModelID string
   SelectionMode         string
   LanguageMode          string
   ConfigLanguageID      string
   EffectiveLanguageID   string
   BackendKind           string
   Provider              string
   LanguageParam         string
   FallbackReason        string
   NumThreads            int
   HardwareInfo          string
}
func RunApp() error {
@@ -132,6 +157,7 @@
   historyService := services.NewHistoryService(app.history)
   userDictService := services.NewUserDictService(app.userdict)
   correctionCSVService := services.NewCorrectionCSVService(app.history, app.userdict)
   diagnosticsService := services.NewDiagnosticsService(app.cfg, appDisplayVersion)
   // Create Wails application
   wailsApp := application.New(application.Options{
@@ -148,6 +174,7 @@
         application.NewService(historyService),
         application.NewService(userDictService),
         application.NewService(correctionCSVService),
         application.NewService(diagnosticsService),
      },
      Assets: application.AssetOptions{
         Handler: application.AssetFileServerFS(assets),
@@ -588,9 +615,20 @@
         case <-a.ctx.Done():
            return
         case <-ticker.C:
            d := time.Since(start)
            a.mu.Lock()
            recording := a.isRecording
            captionActive := a.liveCaptionCancel != nil
            if recording && d >= audio.MaxRecordingDuration {
               logger.Error("Recording reached max duration; auto-stopping max_seconds=%d", int(audio.MaxRecordingDuration/time.Second))
               if a.isFreetalking {
                  a.stopTapRecordingLocked()
               } else {
                  a.stopRecordingLocked(false)
               }
               a.mu.Unlock()
               return
            }
            a.mu.Unlock()
            if !recording {
               return
@@ -598,7 +636,6 @@
            if captionActive {
               continue
            }
            d := time.Since(start)
            a.indicator.SetStatus(status, fmt.Sprintf("%d:%02d", int(d.Minutes()), int(d.Seconds())%60))
         }
      }
@@ -860,8 +897,6 @@
   hotkeyVK := a.cfg.HotkeyVK
   autoHide := a.cfg.AutoHide
   copyToClipboard := a.cfg.CopyToClipboard
   selectedModelID := a.cfg.SelectedModelID
   languageID := a.cfg.LanguageID
   a.mu.Unlock()
   logger.Info(
@@ -893,6 +928,7 @@
   a.engineMu.Lock()
   engineLockElapsed := time.Since(engineLockStart)
   eng := a.eng
   engMeta := a.engineMeta
   if eng == nil {
      a.engineMu.Unlock()
      logger.Error("Recognition skipped: engine not ready")
@@ -982,52 +1018,84 @@
   }
   totalMS := perfSinceMS(pipelineStart)
   logger.Info("PERF pipeline_done id=%d result=success total_ms=%d", perfID, totalMS)
   logSlowRecognitionIfNeeded(perfID, eng, selectedModelID, languageID, samples, recognizeElapsedMS, totalMS)
   logSlowRecognitionIfNeeded(perfID, engMeta, samples, recognizeElapsedMS, totalMS)
   a.delayedHideIf(autoHide, doneIndicatorHideDelayMs)
}
func logSlowRecognitionIfNeeded(perfID int64, eng engine.Engine, modelID, languageID string, samples []float32, recognizeMS, totalMS int64) {
func logSlowRecognitionIfNeeded(perfID int64, meta engineMetadata, samples []float32, recognizeMS, totalMS int64) {
   reason := slowRecognitionReason(recognizeMS, totalMS)
   if reason == "" {
      return
   }
   engineInfo := ""
   if eng != nil {
      engineInfo = eng.HardwareInfo()
   audioMS := perfAudioDurationMS(samples)
   rtf := 0.0
   if audioMS > 0 {
      rtf = float64(recognizeMS) / float64(audioMS)
   }
   numCPU := runtime.NumCPU()
   load, hasLoad := currentSystemLoadAverage()
   if hasLoad {
      loadPerCore := 0.0
      if numCPU > 0 {
         loadPerCore = load[0] / float64(numCPU)
      }
      logger.Info(
         "PERF slow_recognition id=%d reason=%s model_id=%q language_id=%q engine=%q audio_ms=%d samples=%d recognize_ms=%d total_ms=%d load1=%.2f load5=%.2f load15=%.2f",
         "PERF slow_recognition id=%d reason=%s model_id=%q resolved_model_id=%q cfg_selected_model_id=%q selection_mode=%q language_mode=%q config_language_id=%q effective_language_id=%q backend=%q provider=%q language_param=%q fallback_reason=%q num_threads=%d engine=%q init_id=%d audio_ms=%d samples=%d recognize_ms=%d total_ms=%d rtf=%.3f load1=%.2f load5=%.2f load15=%.2f load1_per_core=%.2f cpu_count=%d",
         perfID,
         reason,
         modelID,
         languageID,
         engineInfo,
         perfAudioDurationMS(samples),
         meta.ResolvedModelID,
         meta.ResolvedModelID,
         meta.ConfigSelectedModelID,
         meta.SelectionMode,
         meta.LanguageMode,
         meta.ConfigLanguageID,
         meta.EffectiveLanguageID,
         meta.BackendKind,
         meta.Provider,
         meta.LanguageParam,
         meta.FallbackReason,
         meta.NumThreads,
         meta.HardwareInfo,
         meta.InitID,
         audioMS,
         len(samples),
         recognizeMS,
         totalMS,
         rtf,
         load[0],
         load[1],
         load[2],
         loadPerCore,
         numCPU,
      )
      return
   }
   logger.Info(
      "PERF slow_recognition id=%d reason=%s model_id=%q language_id=%q engine=%q audio_ms=%d samples=%d recognize_ms=%d total_ms=%d load_unavailable=true",
      "PERF slow_recognition id=%d reason=%s model_id=%q resolved_model_id=%q cfg_selected_model_id=%q selection_mode=%q language_mode=%q config_language_id=%q effective_language_id=%q backend=%q provider=%q language_param=%q fallback_reason=%q num_threads=%d engine=%q init_id=%d audio_ms=%d samples=%d recognize_ms=%d total_ms=%d rtf=%.3f load_unavailable=true cpu_count=%d",
      perfID,
      reason,
      modelID,
      languageID,
      engineInfo,
      perfAudioDurationMS(samples),
      meta.ResolvedModelID,
      meta.ResolvedModelID,
      meta.ConfigSelectedModelID,
      meta.SelectionMode,
      meta.LanguageMode,
      meta.ConfigLanguageID,
      meta.EffectiveLanguageID,
      meta.BackendKind,
      meta.Provider,
      meta.LanguageParam,
      meta.FallbackReason,
      meta.NumThreads,
      meta.HardwareInfo,
      meta.InitID,
      audioMS,
      len(samples),
      recognizeMS,
      totalMS,
      rtf,
      numCPU,
   )
}
@@ -1106,18 +1174,22 @@
}
func (a *App) initEngine() {
   logger.Info("Initializing ASR engine...")
   if a.engineService != nil {
      a.engineService.SetStatus("loading", "", "")
   }
   a.wailsApp.Event.Emit("engine:status", map[string]interface{}{
      "status": "loading",
   })
   initID := a.engineInitSeq.Add(1)
   modelExists := engine.ModelExists()
   eng, err := engine.New()
   a.engineInitMu.Lock()
   defer a.engineInitMu.Unlock()
   if initID != a.engineInitSeq.Load() {
      logger.Info("Skipping superseded ASR engine init: init_id=%d current_init_id=%d", initID, a.engineInitSeq.Load())
      return
   }
   logger.Info("Initializing ASR engine... init_id=%d", initID)
   a.setEngineStatus("loading", "", "")
   eng, meta, modelExists, err := a.createEngineForInit(initID)
   if err != nil {
      logger.Error("Engine initialization failed: %v", err)
      logger.Error("Engine initialization failed: init_id=%d model_id=%q error=%v", initID, meta.ResolvedModelID, err)
      if a.hasEngine() {
         logger.Info("Keeping existing ASR engine after reload failure")
      } else {
@@ -1127,26 +1199,24 @@
      if modelExists {
         status = "error"
      }
      if a.engineService != nil {
         a.engineService.SetStatus(status, "", err.Error())
      }
      a.wailsApp.Event.Emit("engine:status", map[string]interface{}{
         "status": status,
         "error":  err.Error(),
      })
      a.setEngineStatus(status, "", err.Error())
      return
   }
   a.replaceEngine(eng)
   logger.Info("ASR engine ready: %s", eng.HardwareInfo())
   if a.engineService != nil {
      a.engineService.SetStatus("ready", eng.HardwareInfo(), "")
   if initID != a.engineInitSeq.Load() {
      logger.Info("Discarding stale ASR engine init: init_id=%d current_init_id=%d model_id=%q", initID, a.engineInitSeq.Load(), meta.ResolvedModelID)
      eng.Close()
      return
   }
   a.wailsApp.Event.Emit("engine:status", map[string]interface{}{
      "status":       "ready",
      "hardwareInfo": eng.HardwareInfo(),
   })
   a.replaceEngineWithMetadata(eng, meta)
   logger.Info("ASR engine ready: init_id=%d model_id=%q backend=%s provider=%s hardware=%s", initID, meta.ResolvedModelID, meta.BackendKind, meta.Provider, eng.HardwareInfo())
   a.setEngineStatus("ready", eng.HardwareInfo(), "")
   if a.indicator == nil {
      return
   }
   // Show indicator briefly — snapshot config under lock
   a.mu.Lock()
@@ -1164,6 +1234,77 @@
   a.delayedHideIf(autoHide, 2000)
}
func (a *App) createEngineForInit(initID uint64) (engine.Engine, engineMetadata, bool, error) {
   if a.engineFactory != nil {
      return a.engineFactory(initID)
   }
   cfg, err := config.Load()
   if err != nil {
      logger.Error("Failed to load config for engine init: %v", err)
      cfg = a.cfg
      if cfg == nil {
         cfg = config.Default()
      }
   }
   current := modelselection.Resolve(cfg, language.NewSystemDetector())
   meta := engineMetadata{
      InitID:                initID,
      ResolvedModelID:       current.ModelID,
      ConfigSelectedModelID: cfg.SelectedModelID,
      SelectionMode:         current.SelectionMode,
      LanguageMode:          config.NormalizeLanguageMode(cfg.LanguageMode),
      ConfigLanguageID:      cfg.LanguageID,
      EffectiveLanguageID:   current.LanguageSettings.EffectiveLanguageID,
      FallbackReason:        current.FallbackReason,
   }
   resolved, err := model.ResolveModel(model.NormalizeModelID(current.ModelID))
   if err != nil {
      return nil, meta, false, err
   }
   meta.ResolvedModelID = resolved.ModelID
   meta.BackendKind = resolved.BackendKind
   meta.LanguageParam = resolved.Profile.LanguageParam
   meta.NumThreads = resolved.Profile.NumThreads
   modelExists := resolved.IsUsable()
   if !modelExists {
      return nil, meta, false, fmt.Errorf("model %s is not usable: %s missing=%v problems=%v", resolved.ModelID, resolved.Status, resolved.Missing, resolved.Problems)
   }
   eng, err := engine.NewWithResolvedModel(resolved)
   if err != nil {
      return nil, meta, modelExists, err
   }
   meta.HardwareInfo = eng.HardwareInfo()
   if providerEng, ok := eng.(engine.ProviderEngine); ok {
      meta.Provider = providerEng.Provider()
   }
   return eng, meta, modelExists, nil
}
func (a *App) setEngineStatus(status, hardwareInfo, errText string) {
   if a.engineService != nil {
      a.engineService.SetStatus(status, hardwareInfo, errText)
   }
   if a.wailsApp == nil {
      return
   }
   payload := map[string]interface{}{
      "status": status,
   }
   if hardwareInfo != "" {
      payload["hardwareInfo"] = hardwareInfo
   }
   if errText != "" {
      payload["error"] = errText
   }
   a.wailsApp.Event.Emit("engine:status", payload)
}
func (a *App) hasEngine() bool {
   a.engineMu.Lock()
   defer a.engineMu.Unlock()
@@ -1171,12 +1312,17 @@
}
func (a *App) replaceEngine(eng engine.Engine) {
   a.replaceEngineWithMetadata(eng, engineMetadata{})
}
func (a *App) replaceEngineWithMetadata(eng engine.Engine, meta engineMetadata) {
   a.releaseTailCaptureNanos.Store(int64(releaseTailCaptureDelayForEngine(eng)))
   a.holdPreCaptureEnabled.Store(holdPreCaptureEnabledForEngine(eng))
   a.engineMu.Lock()
   old := a.eng
   a.eng = eng
   a.engineMeta = meta
   a.engineMu.Unlock()
   if old != nil && old != eng {
@@ -1391,9 +1537,13 @@
   if a.recorder != nil {
      a.recorder.Close()
   }
   if a.hk != nil {
      a.hk.Close()
   }
   a.replaceEngine(nil)
   a.wg.Wait()
   logger.Info("Cleanup complete")
   logger.Close()
}
// SetRecordingHotkey is called by the hotkey service when entering hotkey recording mode.