From fecea2c7a4ecc48b2354868894e78be361a2ccf1 Mon Sep 17 00:00:00 2001
From: Cw <cw@git.boborobots.com>
Date: Sat, 04 Jul 2026 16:53:53 +0800
Subject: [PATCH] Fix Windows ASR model paths for non-ASCII users
---
C1.source/privatevoice.src/app.go | 260 +++++++++++++++++++++++++++++++++++++++++-----------
1 files changed, 205 insertions(+), 55 deletions(-)
diff --git a/C1.source/privatevoice.src/app.go b/C1.source/privatevoice.src/app.go
index 0a0c428..9acb5f6 100755
--- a/C1.source/privatevoice.src/app.go
+++ b/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.
--
Gitblit v1.9.3